Võrguseaded FreeRadius kaudu DHCP kaudu

Võrguseaded FreeRadius kaudu DHCP kaudu
Saime ülesande korraldada IP aadresside jagamine abonentidele. Ülesande tingimused:

  • Me ei anna eraldi serverit autentimiseks — peate hakkama saama 😉
  • Abonentidel peavad olema võrguseaded DHCP kaudu
  • Võrk on mitmekesine. Seal on nii PON seadmed kui ka tavapärased lülitid, kus on seadistatud Valik 82 ja WiFi jaamad koos punktidega
  • Kui ükski IP jagamise tingimus ei ole täidetud, tuleb anda IP aadress „külalis“ võrgust

Heast on siiski olemas server FreeBSD, mis võib „töötada“, aga see on kaugel ;), ei ole „otse selles võrgus“.

Veel on olemas suurepärane seade Mikrotik. Üldine võrgu skeem on enam-vähem järgmine:

Võrguseaded FreeRadius kaudu DHCP kaudu

Veidi mõeldes, otsustati kasutada abonentidele võrgu seadete jagamiseks FreeRadius. Põhimõtteliselt on skeem tavaline: Microticki peal aktiveerime DHCP serveri, sama moodi Radius Clienti. Seadistame sideme DHCP server -> Radius Client -> Radius server.

Tundub, et see ei ole keeruline. Aga! Kurat peitub detailides. Nimelt:

  • PON OLT autentimisel selle skeemi kaudu „tuleb“ FreeRadiusest päring, kus User-Name on võrgu seadme MAC aadress, Agent-Circuit-Id on PON ONU MAC aadress ja parool on tühi.
  • Lülitite autentimisega, kus on valik 82, tuleb FreeRadiusesse päring, kus tühi User-Name on võrgu seadme MAC aadress ja täidetud on täiendavad atribuudid Agent-Circuit-Id ja Agent-Remote-Id, milles vastavalt on taas võrgu seadme MAC aadress ja port, kuhu abonent on ühendatud.
  • Mõned abonendid WiFi punktidest autentivad end läbi PAP-CHAP protokollide
  • Mõned abonendid WiFi punktidest autentivad end User-Name, mis on võrdne WiFi punkti MAC aadressiga, ilma paroolita.

Ajalooline taust: mis on „Valik 82“ DHCP-s

Need on täiendavad valikud protokollis DHCP, mis võimaldavad edastada täiendavat teavet, näiteks väljad Agent-Circuit-Id ja Agent-Remote-Id. Tavaliselt kasutatakse MAC aadressi edastamiseks võrgu seadme ja selle ühendatud sadama kohta. PON seadmete või WiFi baasjaamade puhul ei too Agent-Circuit-Id väli kasulikku teavet (ei ole abonendiporti). Samas on DHCP töö üldine skeem järgmine:

Võrguseaded FreeRadius kaudu DHCP kaudu

Samm-sammult töötab see skeem selliselt:

  1. Abonentide seadmed teevad laiaulatusliku DHCP päringu võrguseadete saamiseks
  2. Seade (ntuks näiteks lüliti, WiFi baasjaam või PON), kuhu abonendi varustus on otseselt ühendatud, "püüab" selle paketi kinni, muudab seda, lisades sellele lisavalikuid Option 82 ja Relay agent IP address, ning edastab selle edasi võrgus.
  3. DHCP server võtab päringu vastu, koostab vastuse ja saadab selle relé seadmele
  4. Relé seade edastab vastuse paketi abonendi seadmele

Kahjuks ei toimi see nii lihtsalt, vajalik on vastav võrguseadmete seadistamine.

FreeRadius installimine

FreeRadius konfiguratsiooni seadistamisega on kindlasti võimalik kõike saavutada, kuid see on keeruline ja arusaamatu... eriti kui sinna minna pärast N kuud, kui "kõik töötab". Seetõttu otsustati kirjutada oma autentimismoodul FreeRadius jaoks Pythonis. Autentimise andmed võtame MySQL andmebaasist. Andmebaasi struktuuri kirjeldamiseks pole mõtet, igaühel on see nagunii "enda järgi". Ma kasutasin struktuuri, mis on FreeRadius'ega koosolevale sql moodulile soovitatud, ja tegin sellele väikese muudatuse, lisades igale abonendile välja mac ja port, lisaks логин-пароль.

Nii et alustame FreeRadius'i installimist:

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

Seadetes märkige installimiseks:

Võrguseaded FreeRadius kaudu DHCP kaudu

Teeme sümboolsed lingid Python mooduli jaoks (st "lülitame" selle sisse):

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

Installime Pythonile täiendava mooduli:

pip install mysql-connector

FreeRadius'i Python mooduli seadetes tuleb võttes seadmete otsinguteed määrata muutuja python_path. Näiteks on minul see:

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"

Teed saab teada, käivitades python interpreteerija ja sisestades käsud:

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

Kui seda sammu ei astu, siis FreeRadiusi peal käivitatud Pythonis kirjutatud skriptid ei leia neid mooduleid, mis on imporditud. Samuti tuleb seadistustes eemaldada kommentaarid autoriseerimise ja arvestuse funktsioonide kutsumise osas. Näiteks on minu moodul selline:

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}

}

Skripti work.py (ja kõiki teisi) tuleb paigutada /usr/local/etc/raddb/mods-config/python. Kokku on mul skripte kolm.

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

Koodist on näha, et me proovime kõikide võimalike meetoditega tuvastada abonenti tema eelnevalt tuntud MAC aadresside või Option 82 kombinatsioonide põhjal, ja kui see ei õnnestu, väljastame kõige vanema varem kasutatud IP aadressi ''külaliste'' võrgus. Jääb veel seadistada skript default kaustas sites-enabled, et vajalikud funktsioonid Pythonis kirjutatud skriptist käivitatakse määratud hetkedel. Tegelikult piisab faili viimist sellisesse vormi:

Seejärel käivitame exec käsu ja ootame 5000 sekundit, et jätkata jälgimist:

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

Proovime käivitada ja vaatame, mis ilmub tõrkelogisse:

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

Mis veel. FreeRadiuse seadistamisel on mugav seda tööd testida radclient utiliidiga. Näiteks autoriseerimise puhul:

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

Või arvestamine:

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

Soovin hoiatada, et sarnaste skeemide ja skriptide rakendamine "muudatuseta" "tööstuslikus" ulatuses ei ole kuidagi lubatud. Mida vähemasti tähelepanu köidab:

  • on võimalik "MAC aadressi vale esitlemine". Kliendil piisab, kui kirjutada endale vale MAC ja probleemid on tagatud.
  • külaliste võrkude jagamise loogika on allpool igasugust kriitikat. Pole isegi kontrolli "kas sellise IP aadressi on juba antud klientidele?"

See on lihtsalt "lahendus käepärast", et see töötaks täpselt minu tingimustes, mitte midagi rohkemat. Ärge hinnake rangelt 😉

Allikas: habr.com

Osta usaldusväärne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster