Impostazioni di rete da FreeRadius tramite DHCP

Impostazioni di rete da FreeRadius tramite DHCP
È arrivata la richiesta di configurare l'emissione degli indirizzi IP agli abbonati. Condizioni della richiesta:

  • Non forniremo un server separato per l'autenticazione — dovrete adattarvi 😉
  • Gli abbonati devono ricevere le impostazioni di rete tramite DHCP
  • La rete è eterogenea. Ci sono sia attrezzature PON che normali switch con l'Opzione 82 configurata e basi WiFi con punti di accesso.
  • Se nessuna delle condizioni per l'emissione degli IP è soddisfatta, è necessario emettere un IP dalla rete 'ospite'.

Dal lato positivo: c'è. server su FreeBSD, che può 'funzionare', ma si trova 'a tre isolette di distanza' ;), non 'proprio in questa rete'.

C'è anche un meraviglioso dispositivo Mikrotik. Lo schema generale della rete è circa questo:

Impostazioni di rete da FreeRadius tramite DHCP

Dopo aver riflettuto, è stata presa la decisione di utilizzare FreeRadius per emettere le impostazioni di rete agli abbonati. In linea di massima, lo schema è normale: accendiamo il server DHCP su Microtik, e su di esso il Client Radius. Configuriamo una connessione server DHCP -> Client Radius -> server Radius.

Sembra non difficile. Ma! Il diavolo sta nei dettagli. E precisamente:

  • Durante l'autenticazione PON OLT secondo questo schema, a FreeRadius arriva una richiesta con User-Name pari all'indirizzo MAC della stazione principale, Agent-Circuit-Id pari all'indirizzo MAC PON Onu e password vuota.
  • Durante l'autenticazione da switch con opzione 82, a FreeRadius arriva una richiesta con User-Name vuoto pari all'indirizzo MAC dell'abbonato e attributi aggiuntivi Agent-Circuit-Id e Agent-Remote-Id riempiti, contenenti rispettivamente anche l'indirizzo MAC dello switch di relay e la porta a cui è collegato l'abbonato.
  • Una parte degli abbonati con i punti WiFi si autentica tramite i protocolli PAP-CHAP.
  • Una parte degli abbonati con i punti WiFi si autentica con User-Name pari all'indirizzo MAC del punto WiFi, senza password.

Note storiche: che cos'è l'«Opzione 82» nel DHCP

Queste sono opzioni aggiuntive del protocollo DHCP che permettono di trasmettere informazioni supplementari, ad esempio nei campi Agent-Circuit-Id e Agent-Remote-Id. Di solito sono usate per il passaggio dell'indirizzo MAC dello switch di relay e della porta a cui è collegato l'abbonato. Nel caso di attrezzature PON o stazioni base WIFI, il campo Agent-Circuit-Id non porta informazioni utili (non c'è porta per l'abbonato). In questo caso, lo schema generale di funzionamento del DHCP è il seguente:

Impostazioni di rete da FreeRadius tramite DHCP

Passo dopo passo, questo schema funziona così:

  1. Le attrezzature degli abbonati fanno una richiesta DHCP broadcast per ricevere le impostazioni di rete.
  2. Il dispositivo (ad esempio uno switch, una stazione base WiFi o un PON) al quale si collega direttamente l'equipaggiamento dell'abbonato "intercetta" questo pacchetto e lo modifica, integrando opzioni aggiuntive Option 82 e indirizzo IP del Relay agent, e lo trasmette ulteriormente nella rete.
  3. Il server DHCP riceve la richiesta, forma la risposta e la invia al dispositivo di relay.
  4. Il dispositivo di relay inoltra il pacchetto di risposta all'apparecchiatura dell'abbonato.

Certo, non funziona tutto così semplicemente; è necessaria un'impostazione appropriata dell'equipaggiamento di rete.

Installazione di FreeRadius

Con le impostazioni di configurazione di FreeRadius è possibile raggiungere tutto ciò, ma è complicato e poco chiaro… soprattutto quando ci si immerge lì dopo N mesi con l'idea che "tutto funziona". Pertanto, è stata presa la decisione di scrivere il proprio modulo di autorizzazione per FreeRadius in Python. I dati per l'autorizzazione verranno presi da un database MySQL. Non ha senso descrivere la sua struttura, ognuno di certo la farà "su misura". In particolare, ho preso la struttura proposta con il modulo sql per FreeRadius e l'ho leggermente modificata, aggiungendo i campi mac e port per ciascun abbonato, oltre a login e password.

Quindi, per cominciare, installiamo FreeRadius:

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

Nelle impostazioni selezioniamo per l'installazione:

Impostazioni di rete da FreeRadius tramite DHCP

Creiamo un symlink per il modulo python (cioè "attiviamo"):

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

Installeremo un modulo aggiuntivo per python:

pip install mysql-connector

Nelle impostazioni del modulo python per FreeRadius, è necessario specificare i percorsi di ricerca dei moduli nella variabile python_path. Ad esempio, per me è questo:

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"

I percorsi possono essere scoperti avviando l'interprete python e digitando i comandi:

root@phaeton:/usr/local/etc/raddb/mods-enabled# python
Python 2.7.15 (default, 8 dic 2018, 01:22:25) 
[GCC 4.2.1 Compatibile FreeBSD Clang 6.0.1 (tags/RELEASE_601/final 335540)] su freebsd12
Digita "help", "copyright", "credits" o "license" per ulteriori informazioni.
>>> 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']
>

Se non si compie questo passo, gli script scritti in python e avviati con FreeRadius non troveranno quei moduli elencati in import. Inoltre, è necessario decommentare nelle impostazioni del modulo le funzioni di chiamata per l'autenticazione e la contabilità. Ad esempio, il mio modulo appare così:

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}

}

Lo script work.py (e tutti gli altri) deve essere posizionato in /usr/local/etc/raddb/mods-config/python. In totale ho creato tre script.

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

Come si può vedere dal codice, stiamo cercando di identificare l'abbonato in tutti i modi possibili, utilizzando i suoi indirizzi MAC già noti o una combinazione di Option 82, e se questo non funziona, forniamo il più vecchio degli indirizzi IP utilizzati in precedenza dalla rete "ospite". Rimane da configurare lo script default nella cartella sites-enabled, in modo che le funzioni necessarie dello script python vengano chiamate nei momenti opportuni. In pratica, è sufficiente portare il file alla seguente forma:

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

Proviamo a eseguire e vedere cosa arriva nel log di debug:

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

Cos'altro. Nella configurazione di FreeRadius è utile testarne il funzionamento tramite l'utilità radclient. Ad esempio, per l'autenticazione:

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

Oppure contabilità:

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

Voglio avvertire che utilizzare uno schema simile e script "senza modifiche" su scala "industriale" non è affatto consigliabile. Perlomeno si notano subito:

  • è possibile la "falsificazione" dell'indirizzo MAC. È sufficiente che l'utente si imposti un indirizzo MAC estraneo e ci saranno problemi
  • la logica di assegnazione delle reti ospiti è sotto ogni critica. Non c'è nemmeno un controllo "ci sono già clienti a cui è stato assegnato tale indirizzo IP?"

È semplicemente una "soluzione improvvisata", realizzata per funzionare specificamente nelle mie condizioni, nulla di più. Non giudicate troppo severamente 😉

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster