Netwurkynstellingen fan FreeRadius fia DHCP

Netwurkynstellingen fan FreeRadius fia DHCP
De taak kaam om de útjefte fan IP-adressen oan abonnees te regeljen. Betingsten fan it probleem:

  • Wy sille jo gjin aparte tsjinner jaan foar autorisaasje - jo sille it dwaan 😉
  • Abonnees moatte netwurkynstellingen krije fia DHCP
  • It netwurk is heterogeen. Dit omfettet PON-apparatuer en reguliere skeakels mei konfigureare opsje 82 en WiFi-bases mei hotspots
  • As de gegevens net falle ûnder ien fan 'e betingsten foar it útjaan fan in IP, moatte jo in IP útjaan fan it "gast" netwurk

Oan 'e goede kant: d'r is noch in server op FreeBSD dy't kin "wurkje", mar it is "fier fuort" ;), net "rjocht op dit netwurk".

Der is ek in prachtich apparaat neamd Mikrotik. It algemiene netwurkdiagram is sa:

Netwurkynstellingen fan FreeRadius fia DHCP

Nei wat tinken waard besletten om FreeRadius te brûken om netwurkynstellingen út te jaan oan abonnees. Yn prinsipe is it skema gewoanlik: wy aktivearje de DHCP-tsjinner op Microtick, en Radius Client derop. Wy konfigurearje de DHCP-tsjinner -> Radius Client -> Radius-tsjinnerferbining.

It liket net dreech. Mar! De duvel is yn 'e details. Nammentlik:

  • By it autorisearjen fan in PON OLT mei dit skema, wurdt in fersyk stjoerd nei FreeRadius mei in brûkersnamme lyk oan it MAC-adres fan 'e koptekst, in Agent-Circuit-Id lyk oan de MAC PON Onu en in leech wachtwurd.
  • By it autorisearjen fan skeakels mei opsje 82, ûntfangt FreeRadius in fersyk mei in lege brûkersnamme lyk oan de MAC fan it apparaat fan 'e abonnee en fol mei ekstra attributen Agent-Circuit-Id en Agent-Remote-Id dy't respektivelik wer de MAC fan befetsje. de estafette switch en de haven dêr't de abonnee is ferbûn.
  • Guon abonnees mei WiFI-punten binne autorisearre fia PAP-CHAP-protokollen
  • Guon abonnees fan WIFI-punten binne autorisearre mei in brûkersnamme lyk oan it MAC-adres fan it WIFI-punt, sûnder wachtwurd.

Histoaryske eftergrûn: wat is "Opsje 82" yn DHCP

Dit binne ekstra opsjes foar it DHCP-protokol wêrmei jo ekstra ynformaasje kinne oerdrage, bygelyks yn 'e fjilden Agent-Circuit-Id en Agent-Remote-Id. Typysk brûkt foar it ferstjoeren fan it MAC-adres fan 'e estafette-skeakel en de poarte wêrmei't de abonnee is ferbûn. Yn it gefal fan PON-apparatuer as WIFI-basisstasjons befettet it fjild Agent-Circuit-Id gjin nuttige ynformaasje (d'r is gjin abonneepoarte). It algemiene skema fan DHCP-operaasje yn dit gefal is as folget:

Netwurkynstellingen fan FreeRadius fia DHCP

Stap foar stap wurket dit skema sa:

  1. De brûkersapparatuer makket in DHCP-útstjoerfersyk om netwurkynstellingen te krijen
  2. It apparaat (bgl it netwurk.
  3. De DHCP-tsjinner akseptearret it fersyk, genereart in antwurd en stjoert it nei it relay-apparaat
  4. It relay-apparaat stjoert it antwurdpakket troch nei it abonnee-apparaat

Fansels wurket it allegear net sa maklik; jo moatte jo netwurkapparatuer neffens konfigurearje.

FreeRadius ynstallearje

Fansels kin dit wurde berikt mei de FreeRadius-konfiguraasje-ynstellingen, mar it is lestich en ûndúdlik ... foaral as jo nei N moannen dêr hinne gean en "alles wurket." Dêrom hawwe wy besletten om ús eigen autorisaasjemodule foar FreeRadius yn Python te skriuwen. Wy sille autorisaasjegegevens nimme fan 'e MySQL-database. D'r hat gjin punt om de struktuer te beskriuwen; yn elk gefal sil elkenien it "foar harsels" meitsje. Benammen naam ik de struktuer dy't wurdt oanbean mei de sql-module foar FreeRadius, en feroare it in bytsje troch in mac- en poartefjild foar elke abonnee ta te foegjen, neist it login-wachtwurd.

Sa, earst, ynstallearje FreeRadius:

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

Selektearje yn 'e ynstellings om te ynstallearjen:

Netwurkynstellingen fan FreeRadius fia DHCP

Wy meitsje in symlink nei de pythonmodule (dat wol sizze "ynskeakelje"):

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

Litte wy in ekstra module foar python ynstallearje:

pip install mysql-connector

Yn 'e python-module-ynstellingen foar FreeRadius moatte jo de module-sykpaden yn' e python_path-fariabele opjaan. Ik haw bygelyks dit:

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"

Jo kinne de paden útfine troch de python-tolk te starten en de kommando's yn te fieren:

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

As jo ​​​​dizze stap net nimme, dan sille skripts skreaun yn python en lansearre troch FreeRadius de modules net fine dy't yn ymport steane. Derneist moatte jo de funksjes foar it oproppen fan autorisaasje en boekhâlding yn 'e module-ynstellingen unkommentearje. Bygelyks, dizze module sjocht der sa út:

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}

}

It work.py-skript (en alle oaren) moatte pleatst wurde yn /usr/local/etc/raddb/mods-config/python Ik haw trije skripts yn totaal.

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

Lykas jo kinne sjen út 'e koade, besykje wy de abonnee te identifisearjen mei alle beskikbere metoaden troch syn bekende MAC-adressen fan abonnees as opsje 82-kombinaasje, en as dit net wurket, jouwe wy it âldste IP-adres dat ea brûkt is út 'e "gast" ” netwurk. Alles wat oerbliuwt is it standertskript yn 'e map mei siden ynskeakele te konfigurearjen, sadat de nedige funksjes fan it python-skript op de oanwiisde mominten twitchje. Yn feite is it genôch om it bestân nei it formulier te bringen:

standert

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

Litte wy besykje it út te fieren en te sjen wat der yn it debuglog komt:

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

Wat oars. By it ynstellen fan FreeRadius is it handich om de wurking te testen mei it radclient-hulpprogramma. Bygelyks autorisaasje:

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

Of akkount:

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

Ik wol jo warskôgje dat it absolút ûnmooglik is om sa'n skema en skripts "sûnder feroaringen" te brûken op in "yndustriële" skaal. Op syn minst merkber:

  • it is mooglik om it MAC-adres "fake". It is genôch foar de abonnee om de MAC fan in oar te registrearjen en d'r sille problemen wêze
  • de logika fan it útjaan fan gastnetwurken is boppe krityk. D'r is net iens in kontrôle "miskien binne d'r al kliïnten mei itselde IP-adres?"

Dit is gewoan in "cookie-cutter-oplossing" ûntworpen om spesifyk te wurkjen yn myn betingsten, neat mear. Rjochtsje net strang 😉

Boarne: www.habr.com

Add a comment