Nettverksinnstillinger fra FreeRadius via DHCP

Nettverksinnstillinger fra FreeRadius via DHCP
Oppgaven kom opp om å sette opp IP-adresseutdeling til abonnenter. Oppgavens betingelser:

  • Vi vil ikke gi en egen server for autentisering – du klarer deg 😉
  • Abonnenter må motta nettverksinnstillinger via DHCP
  • Nettverket er variert. Dette inkluderer både PON-utstyr og vanlige switcher med konfigurert Alternativ 82, samt WiFi-baser med punkter.
  • Hvis ingen av betingelsene for IP-utdeling er oppfylt, må det tildeles en IP fra gjestenettverket.

Det positive: det finnes faktisk server på FreeBSD, som kan "arbeide", men det er "langt borte" ;), ikke "direkte i dette nettverket".

Det finnes også en fantastisk enhet fra Mikrotik. Den generelle nettverksplanen ser omtrent slik ut:

Nettverksinnstillinger fra FreeRadius via DHCP

Etter å ha reflektert litt, ble det besluttet å bruke FreeRadius til å dele ut nettverksinnstillinger til abonnentene. I bunn og grunn er oppsettet vanlig: på Microtick aktiverer vi DHCP-serveren, og vi konfigurerer også Radius-klienten. Vi setter opp koblingen DHCP-server -> Radius-klient -> Radius-server.

Det virker ikke så vanskelig. Men! Detaillene kan være skjulte. Nærmere bestemt:

  • Ved autentisering av PON OLT på denne måten, "ankommer" en forespørsel til FreeRadius med User-Name lik MAC-adressen til hovedstasjonen, Agent-Circuit-Id lik MAC på PON Onu, og et tomt passord.
  • Ved autentisering med switcher med alternativ 82, kommer en forespørsel til FreeRadius med et tomt User-Name lik MAC-adressen til abonnentens enhet, og med utfylte tilleggsegenskaper Agent-Circuit-Id og Agent-Remote-Id som inneholder henholdsvis MAC-adressen til relésvitsj og porten abonnenten er koblet til.
  • Noen abonnenter fra WiFi-punktene autentiseres via PAP-CHAP-protokoller.
  • Noen abonnenter fra WIFI-punktene autentiseres med User-Name lik MAC-adressen til WiFi-punktet, uten passord.

Historisk oversikt: Hva er «Alternativ 82» i DHCP?

Dette er ekstra alternativer i DHCP-protokollen som gjør det mulig å overføre tilleggsinformasjon, for eksempel i feltene Agent-Circuit-Id og Agent-Remote-Id. Vanligvis brukes det for å overføre MAC-adressen til relésvitsjen og porten abonnenten er koblet til. I tilfelle av PON-utstyr eller WiFi-baser, inneholder feltet Agent-Circuit-Id ingen nyttig informasjon (ingen port for abonnenten). Den generelle driftsmodellen for DHCP i dette tilfellet er som følger:

Nettverksinnstillinger fra FreeRadius via DHCP

Trinnvis fungerer denne modellen slik:

  1. Abonnentutstyret sender en bredcast DHCP-forespørsel for å motta nettverksinnstillinger.
  2. Enheten (for eksempel en switch, WiFi-basestasjon eller PON) som den abonnentutstyret er direkte koblet til, «fanger» denne pakken og endrer den ved å implementere ekstra opsjoner Option 82 og Relay agent IP-adresse, og sender den videre i nettverket.
  3. DHCP-serveren mottar forespørselen, lager et svar og sender det til relé-enheten.
  4. Relé-enheten videresender svarpakken til abonnentutstyret.

Det fungerer selvsagt ikke så enkelt, det kreves riktig konfigurasjon av nettverksutstyret.

Installasjon av FreeRadius.

Med konfigurasjonsinnstillingene for FreeRadius kan man selvsagt oppnå dette, men det er komplisert og ikke lett å forstå … spesielt når man logger seg inn etter flere måneder med «alt fungerer». Derfor ble det besluttet å skrive sin egen autentiseringsmodul for FreeRadius i Python. Dataene for autentisering vil bli hentet fra MySQL-databasen. Det er ikke mening å beskrive strukturen, da alle uansett vil lage den «tilpasset seg». Jeg har faktisk brukt strukturen som foreslås med SQL-modulen for FreeRadius, og gjort noen endringer ved å legge til felt for mac og port for hver abonnent, i tillegg til brukernavn og passord.

La oss begynne med å installere FreeRadius:

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

I innstillingene merker vi av for installasjon:

Nettverksinnstillinger fra FreeRadius via DHCP

Lag en symlink for python-modulen (det vil si at vi «aktiverer» den):

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

La oss installere en ekstra modul for python:

pip install mysql-connector

I innstillingene for python-modulen for FreeRadius, må stiene for modulene legges inn i variabelen python_path. For eksempel har jeg:

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"

Stiene kan man finne ved å starte python-interpreteren og skrive inn kommandoene:

root@phaeton:/usr/local/etc/raddb/mods-enabled# python
Python 2.7.15 (default, 8. des 2018, 01:22:25) 
[GCC 4.2.1 Kompatibel FreeBSD Clang 6.0.1 (tags/RELEASE_601/final 335540)] på freebsd12
Skriv "help", "copyright", "credits" eller "license" for mer informasjon.
>>> 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']
>

Dersom du ikke gjør dette steget, vil skriptene skrevet i Python og kjørt av FreeRadius ikke finne de modulene som er oppført i import. I tillegg må funksjonene for autentisering og regnskap i modulinnstillingene avkommenteres. For eksempel ser min modul slik ut:

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}

}

Skriptene work.py (og alle de andre) må plasseres i /usr/local/etc/raddb/mods-config/python. Totalt har jeg fått tre skripter.

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

Som det fremgår av koden, prøver vi med alle tilgjengelige metoder å identifisere abonnenten ved hjelp av de kjent abonnent MAC-adressene eller kombinasjonen av Option 82, og hvis dette ikke lykkes, gir vi den eldste IP-adressen som noen gang er brukt fra «gjestenettverket». Det gjenstår å konfigurere skriptet default i mappen sites-enabled, slik at de nødvendige funksjonene fra Python-skriptet blir kalt til de angitte tidspunktene. Det er faktisk nok å tilpasse filen til utseendet:

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

La oss prøve å starte opp og se hva som kommer i feilloggen:

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

Hva mer? Når du konfigurerer FreeRadius er det praktisk å teste dets funksjon ved hjelp av verktøyet radclient. For eksempel autentisering:

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

Eller regnskap:

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

Jeg vil advare om at det ikke er lov å bruke slike skjemaer og skript «uten endringer» i «industriell» skala. Det er minst åpenbare problemer:

  • Muligheten for å «forfalske» MAC-adressen. Det er nok for abonnenten å sette inn en annen MAC-adresse, og det vil oppstå problemer.
  • Logikken bak tildeling av gjestenettverk er under enhver kritikk. Det sjekkes ikke engang «har det allerede kunder med tildelt slik IP-adresse?»

Dette er bare en «løsning på kne», for at det skulle fungere spesifikt under mine forhold, ikke mer enn det. Vær ikke for streng 😉

Kilde: habr.com

Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere 🔥 Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere | ProHoster