Cilësimet e rrjetit nga FreeRadius përmes DHCP

Cilësimet e rrjetit nga FreeRadius përmes DHCP
Kemi marrë një detyrë për të konfiguruar ndarjen e adresave IP për abonentët. Kushtet e detyrës:

  • Nuk do të sigurojmë një server të veçantë për autorizimin - do të përballeni me këtë 😉
  • Abonentët duhet të marrin konfigurimet e rrjetit përmes DHCP
  • Rrjeti është heterogjen. Këtu ka pajisje PON, switch normalë me Opsionin 82 të konfiguruar dhe pika WiFi.
  • Nëse të dhënat për ndarjen e adresave IP nuk i përmbushin asnjë nga kushtet - duhet të jepet një IP nga rrjeti 'i mysafirëve'

Nga gjërat pozitive: ka një server në FreeBSD, që mund të 'punojë', por është 'përtej nëntë maleve' ;), jo 'në këtë rrjet'.

Ka gjithashtu një pajisje të shkëlqyer Mikrotik. Skema e përgjithshme e rrjetit është përafërsisht kështu:

Cilësimet e rrjetit nga FreeRadius përmes DHCP

Pas disa reflektimesh, u mor vendimi për të përdorur FreeRadius për ndarjen e konfigurimeve të rrjetit për abonentët. Në thelb, skema është e zakonshme: në Microtik aktivizojmë serverin DHCP, dhe gjithashtu klientin Radius. Konfigurojmë lidhjen DHCP server -> Radius Client -> Radius server.

Nuk duket e ndërlikuar. Por! Djalli është në detaje. Dhe konkretisht:

  • Me autorizimin e PON OLT sipas kësaj skeme, FreeRadius 'merr' një kërkesë me User-Name të barabartë me adresën MAC të stacionit kryesor, Agent-Circuit-Id të barabartë me MAC të PON Onu dhe një fjalëkalim bosh.
  • Me autorizimin nga switch-et me opsion 82, në FreeRadius vjen një kërkesë me User-Name bosh, të barabartë me adresën MAC të pajisjes së abonentëve dhe me atributet shtesë Agent-Circuit-Id dhe Agent-Remote-Id të mbushura, të cilat përmbajnë përkatësisht prapë adresën MAC të switch-it të transmetimit dhe portin ku është lidhur abonenti.
  • Një pjesë e abonentëve me pikat WiFi autorizohen përmes protokolleve PAP-CHAP
  • Një pjesë e abonentëve me pikat WIFI autorizohen me User-Name të barabartë me adresën MAC të pikes WIFI, pa fjalëkalim.

Referencë historike: çfarë është 'Opsioni 82' në DHCP

Këto janë opsione shtesë në protokollin DHCP që lejojnë transmetimin e informacionit shtesë, për shembull në fushat Agent-Circuit-Id dhe Agent-Remote-Id. Përdoret zakonisht për të transmetuar adresën MAC të switch-it të transmetimit dhe portin ku është lidhur abonenti. Në rastin e pajisjeve PON ose stacioneve bazë WiFi, fusha Agent-Circuit-Id nuk sjell informacion të dobishëm (nuk ka port për abonentin). Në këtë rast, skema e përgjithshme e funksionimit të DHCP është kështu:

Cilësimet e rrjetit nga FreeRadius përmes DHCP

Kjo skemë funksionon hap pas hAPI:

  1. Pajisja e abonentit bën një kërkesë DHCP të transmetueshme për të marrë konfigurimet e rrjetit
  2. Pajisja (për shembull, switch-i, stacioni bazë WiFi ose PON) me të cilën pajisja e abonentit është lidhur 'kap' këtë paketë dhe e ndryshon, duke shtuar opsionet e tjera Opsion 82 dhe adresën IP të agjentit të transmetimit, dhe e dërgon atë më tej në rrjet.
  3. Serveri DHCP pranon kërkesën, formon një përgjigje dhe ia dërgon pajisjes transmetuese
  4. Pajisja transmetuese dërgon paketën e përgjigjes në pajisjen e abonentit

Natyrisht, nuk funksionon aq thjesht, kërkohet konfigurimi i duhur i pajisjeve të rrjetit.

Instalimi i FreeRadius

Me konfigurimin e FreeRadius, kjo mund të arrihet, por është e komplikuar dhe e paqartë... sidomos kur e shkoni atje pas N muajsh 'gjithçka funksionon'. Prandaj, u mor vendimi për të shkruar modulin tonë të autorizimit për FreeRadius në Python. Të dhënat për autorizimin do t'i marrim nga baza MySQL. Nuk ka kuptim ta përshkruaj strukturën e saj, sepse çdo njeri do ta bëjë 'sipër vete'. Konkretisht, kam marrë strukturën që ofrohet me modul sql për FreeRadius, dhe e kam ndryshuar pak, duke shtuar fushat mac dhe port për çdo abonent, përveç emrit dhe fjalëkalimit.

Tani, së pari instalojmë FreeRadius:

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

Në cilësimet e instalimit, markojmë për të instaluar:

Cilësimet e rrjetit nga FreeRadius përmes DHCP

Bëjmë një simlink për modul python (dmth. e 'aktivizojmë' atë):

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

Do të instalojmë një modul shtesë për python:

pip install mysql-connector

Në cilësimet e moduli python për FreeRadius, duhet të shkruajmë rrugët e kërkimit të moduleve në variablën python_path. Për shembull, për mua kjo ishte:

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"

Rrugët mund të merren duke kaluar interpretorin python dhe duke futur komandat:

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ëse nuk e bëni këtë hap, atëherë skriptet e shkruara në python dhe të nisi nga FreeRadius nuk do të gjejnë modulat që janë renditur në import. Për më tepër, është e nevojshme të komentoni në cilësimet e modulit funksionet e autorizimit dhe llogarive. Për shembull, moduli im duket kështu:

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}

}

Skema work.py (dhe të gjitha të tjerat) duhet të vendoset në /usr/local/etc/raddb/mods-config/python. Përveç të gjithë skenareve, kam dalë me tre në total.

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

Siç shihet nga kodi, ne përpiqemi të identifikojmë abonentin në mënyra të ndryshme duke iu referuar adresave MAC të njohura ose grupit të Option 82, dhe nëse kjo nuk funksionon, ne lëshojmë adresën IP më të vjetër të përdorur ndonjëherë nga rrjetet "mysafir". Tani mbetet të konfigurohet skenari default në folderin sites-enabled, që funksionet përkatëse nga skenari në python të thirren në momentet e caktuara. Në fakt, mjafton të transformoni skedarin në këtë formë:

default

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

Po e provojmë të nisim dhe të shohim se çfarë mbërrin në logun e debug:

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

E tjera: gjatë konfigurimit të FreeRadius është e lehtë të testosh funksionimin e tij me ndihmën e mjetit radclient. Për shembull, autorizimi:

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

Ose kontabiliteti:

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

Dua të paralajmëroj se përdorimi i një skeme të tillë dhe skenareve "pa ndryshime" në shkallë "industriale" nuk është aspak i përshtatshëm. Të paktën bie në sy:

  • është e mundur "falsifikimi" i adresës MAC. Mjafton që abonenti të vendosë një adresë MAC të huaj dhe do të ketë probleme.
  • logjika e lëshimit të rrjeteve mysafire është poshtë kritikes. Nuk ka asnjë verifikim "a mund të ketë tashmë klientë me një adresë IP të tillë të dhënë?"

Kjo është thjesht "zgjidhje në kujdes", për të funksionuar në kushtet e mia specifike, asgjë më shumë. Mos gjykoni ashpër 😉

Burimi: habr.com

Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS 🔥 Bleni hostim të besueshëm për faqe me mbrojtje nga DDoS, serverë VPS VDS | ProHoster