Mrežne postavke iz FreeRadius preko DHCP

Mrežne postavke iz FreeRadius preko DHCP
Dobio sam zadatak da postavim isporuku IP adresa korisnicima. Uslovi zadatka:

  • Nećemo dodati poseban server za autorizaciju — snaći ćete se 😉
  • Korisnici bi trebali dobijati mrežne postavke putem DHCP-a
  • Mreža je raznolika. Tu su PON uređaji, obični switch-evi sa postavljenom Opcijom 82 i WiFi baze sa tačkama
  • Ako nijedno od uslova za isporuku IP podataka ne bude ispunjeno — potrebno je isporučiti IP iz „gostujuće“ mreže

Sa dobre strane: ipak postoji server na FreeBSD-u, koji može „raditi“, ali je „daleko“ ;), ne „direktno u ovoj mreži“.

Još imamo sjajan uređaj Mikrotik. Opšta shema mreže je otprilike ovakva:

Mrežne postavke iz FreeRadius preko DHCP

Nakon malo razmišljanja, doneta je odluka da se za isporuku mrežnih postavki korisnicima koristi FreeRadius. U suštini, shema je obična: na Mikrotiku uključujemo DHCP server, a na njemu i Radius Client. Podešavamo vezu DHCP server -> Radius Client -> Radius server.

Izgleda da nije teško. Ali! Đavo je u detaljima. A posebno:

  • Prilikom autorizacije PON OLT prema ovoj shemi na FreeRadius dolazi zahtev sa User-Name koji je jednak MAC adresi glavne stanice, Agent-Circuit-Id koji je jednak MAC-u PON Onu i praznom lozinkom.
  • Prilikom autorizacije sa switch-eva sa opcijom 82, na FreeRadius dolazi zahtev sa praznim User-Name koji je jednak MAC adresi uređaja korisnika i popunjenim dodatnim atributima Agent-Circuit-Id i Agent-Remote-Id koji sadrže, opet, MAC adrese relejnog switch-a i port na koji je korisnik priključen.
  • Deo korisnika sa WiFi tačkama autentifikuje se putem PAP-CHAP protokola
  • Deo korisnika sa WiFi tačkama autentifikuje se sa User-Name koji je jednak MAC adresi WiFi tačke, bez lozinke.

Istorijska napomena: šta je „Option 82“ kod DHCP-a

To su dodatne opcije kod DHCP protokola koje omogućavaju prenos dodatnih informacija, na primer u poljima Agent-Circuit-Id i Agent-Remote-Id. Obično se koristi za prenos MAC adrese relejnog switch-a i porta na koji je korisnik priključen. U slučaju PON opreme ili WiFi baznih stanica, polje Agent-Circuit-Id ne nosi korisne informacije (nema porta korisnika). U tom slučaju, opšta shema rada DHCP-a je sledeća:

Mrežne postavke iz FreeRadius preko DHCP

Korak po korak, ova shema funkcioniše ovako:

  1. Korisnička oprema šalje širok DHPC zahtev za dobijanje mrežnih postavki
  2. Uređaj (kao što su switchevi, WiFi bazne stanice ili PON) na koji se direktno povezuje korisnička oprema "zaplijenjuje" ovaj paket i mijenja ga, uvodeći dodatne opcije Option 82 i Relay agent IP address, te ga prosljeđuje dalje kroz mrežu.
  3. DHCP server prihvaća zahtjev, formira odgovor i šalje ga relejnom uređaju.
  4. Relejni uređaj prosljeđuje paket odgovora na korisnički uređaj.

Naravno, nije sve tako jednostavno, potrebna je odgovarajuća konfiguracija mrežne opreme.

Instalacija FreeRadius-a.

Naravno, moguće je postići sve ovo konfiguracijama FreeRadius-a, ali je složeno i nejasno... posebno kada se upustite u to nakon N mjeseci kada "sve radi". Stoga je donesena odluka da se napiše vlastiti modul za autentifikaciju za FreeRadius u Pythonu. Podatke za autentifikaciju ćemo uzimati iz MySQL baze. Nema smisla opisivati strukturu, ionako će svatko raditi "po svom ukusu". Konkretno, uzeo sam strukturu koja se nudi s sql modulom za FreeRadius i malo ju promijenio dodajući pole mac i port za svakog korisnika, pored korisničkog imena i lozinke.

Dakle, prvo instaliramo FreeRadius:

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

U postavkama označavamo za instalaciju:

Mrežne postavke iz FreeRadius preko DHCP

Napravimo simboličku poveznicu na Python modul (tj. "aktiviramo" ga):

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

Instališemo dodatni modul za Python:

pip install mysql-connector

U postavkama Python modula za FreeRadius, potrebno je navesti putanje za pretraživanje modula u varijabli python_path. Na primjer, kod mene je 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"

Putanje se mogu saznati pokretanjem Python interpretatora i unosom komandi:

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

Ako ne poduzmete ovaj korak, skripte napisane u Pythonu koje pokreće FreeRadius neće pronaći module navedene u importu. Također, potrebno je odkomentirati u postavkama modula funkcije za autorizaciju i obračun. Na primjer, moj modul izgleda ovako:

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}

}

Skriptu work.py (i sve ostale) potrebno je staviti u /usr/local/etc/raddb/mods-config/python. Ukupno imam tri skripte.

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

Kao što se može vidjeti iz koda, na sve moguće načine pokušavamo identificirati korisnika prema njegovim unaprijed poznatim MAC adresama ili kombinaciji Option 82, a ako to ne uspije, izdamo najstariju od korištenih IP adresa iz "gostujuće" mreže. Ostaje postaviti skriptu default u mapi sites-enabled, kako bi potrebne funkcije iz skripte na Pythonu bile pozvane u označenim trenucima. U suštini, dovoljno je datoteku prilagoditi da izgleda ovako:

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

Pokušavamo pokrenuti i pogledati što dolazi u dijagnostičkom logu:

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

Što još. Prilikom postavljanja FreeRadius-a, korisno je testirati njegov rad pomoću alata radclient. Na primjer, autorizacija:

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

Ili računovodstvo:

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

Želim naglasiti da se slične sheme i skripte "bez promjena" u "industrijskim" razmjerima nikako ne smiju koristiti. Kao minimum, upada u oči:

  • moguća je "falsifikacija" MAC adrese. Dovoljno je da pretplatnik zada sebi tuđi MAC i nastat će problemi
  • logika izdavanja gostujućih mreža je ispod svake kritike. Nema ni provjere "ima li već klijenata kojima je dodijeljena takva IP adresa?"

To je samo "rješenje na brzinu", da bi funkcioniralo konkretno u mojim uvjetima, ništa više. Nemojte biti strogi 😉

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster