Мрежови настройки от FreeRadius чрез DHCP

Мрежови настройки от FreeRadius чрез DHCP
Получена задача за предоставяне на IP адреси на абонатите. Условия за изпълнение на задачата:

  • Няма да предоставим отделен сървър за удостоверяване — ще се оправите 😉
  • Абонатите трябва да получават мрежови настройки чрез DHCP
  • Мрежата е хетерогенна. Това са и PON устройства, и обикновени суичове с настроена Опция 82 и WiFi бази с точки
  • Ако нито едно от условията за предоставяне на IP не е изпълнено — трябва да се предостави IP от «гостовата» мрежа

От доброто: всъщност има сървър на FreeBSD, който може «да работи», но той е «в трите девет земи» ;), не «точно в тази мрежа».

Има и прекрасно устройство Mikrotik. Общата схема на мрежата е приблизително такава:

Мрежови настройки от FreeRadius чрез DHCP

След известно размисъл, беше взето решение да се използва FreeRadius за предоставяне на мрежови настройки на абонатите. По принцип схемата е обикновена: на Microtick включваме DHCP сървър, на него - Radius клиент. Настройваме връзката DHCP сървър -> Radius клиент -> Radius сървър.

Не изглежда сложно. Но! Дяволът е в детайлите. А именно:

  • При удостоверяване на PON OLT по тази схема на FreeRadius идва заявка с User-Name, равен на MAC адреса на главната станция, Agent-Circuit-Id, равен на MAC на PON Onu, и празна парола.
  • При удостоверяване от суичове с опция 82, на FreeRadius идва заявка с празен User-Name, равен на MAC адреса на абонатното устройство, и запълнени допълнителни атрибути Agent-Circuit-Id и Agent-Remote-Id, които съдържат съответно MAC адреса на релейния суич и порта, към който е свързан абонатът.
  • Част от абонатите с WiFi точки са удостоверени чрез PAP-CHAP протоколи
  • Част от абонатите с WiFi точки се удостоверяват с User-Name, равен на MAC адреса на WiFi точката, без парола.

Историческа информация: какво е «Опция 82» при DHCP

Това са допълнителни опции при протокола DHCP, които позволяват предаване на допълнителна информация, например в полетата Agent-Circuit-Id и Agent-Remote-Id. Обикновено се използва за предаване на MAC адреса на релейния суич и порта, към който е свързан абонатът. В случая на PON оборудване или основни WiFi станции полето Agent-Circuit-Id не носи полезна информация (няма порт на абоната). При това общата схема на работа на DHCP в този случай е следната:

Мрежови настройки от FreeRadius чрез DHCP

Стъпка по стъпка тази схема работи така:

  1. Абонатното оборудване извършва широковещателно DHCP запитване за получаване на мрежови настройки.
  2. Устройството (например суич, базова станция WiFi или PON), към което директно се свързва абонентското оборудване, "прихваща" този пакет и го променя, внедрявайки допълнителни опции Option 82 и Relay agent IP address, след което го предава по мрежата.
  3. DHCP сървърът приема заявката, формулира отговор и го изпраща на релейното устройство.
  4. Релейното устройство препраща пакета с отговора на абонентското устройство.

Така просто всичко това разбира се не работи, необходима е съответна настройка на мрежовото оборудване.

Инсталиране на FreeRadius.

С настройките на конфигурацията на FreeRadius, разбира се, всичко това може да се постигне, но е сложно и не е ясно... особено когато се закараш там след N месеца, когато "всичко работи". Точно затова бе взето решение да се напише собствен модул за авторизация за FreeRadius на Python. Данните за авторизация ще вземем от базата MySQL. Няма смисъл да описвам структурата ѝ, все пак всеки ще я направи "по себе си". По-специално, взех структурата, която се предлага с модула sql за FreeRadius, и я промених малко, добавяйки поле mac и port за всеки абонат, освен потребителското име и парола.

Така че, за начало инсталираме FreeRadius:

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

В настройките отбелязваме за инсталиране:

Мрежови настройки от FreeRadius чрез DHCP

Правим симлинк за модула python (т.е. "включваме" го):

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

Ще инсталираме допълнителен модул за python:

pip install mysql-connector

В настройките на модула python за FreeRadius, трябва да запишете пътищата за търсене на модули в променливата python_path. Например, при мен е:

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"

Пътищата могат да бъдат проверени, стартирайки интерпретатора python и въвеждайки команди:

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

Ако не предприемете тази стъпка, скриптовете написани на python и стартирани FreeRadius няма да намерят модулите, изброени в import. Освен това, е необходимо да разкоментирате настройките на модула за функциите за извикване на авторизация и отчитане. Например, моят модул изглежда така:

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}

}

Скриптът work.py (и всички останали) трябва да се поставят в /usr/local/etc/raddb/mods-config/python. Общо имам три скрипта.

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

Както се вижда от кода, ние се опитваме да идентифицираме абонента по известните му MAC адреси или по комбинирани Option 82 и ако това не се получи, даваме най-стария IP адрес, използван някога в «гостовата» мрежа. Остава да настроим скрипта default в папката sites-enabled, така че необходимите функции от скрипта на python да се извикват в определените моменти. Всъщност, достатъчно е файла да се приведe в следния вид:

по подразбиране

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

Опитваме се да стартираме и да видим какво влиза в отладъчния лог:

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

Освен това, при настройка на FreeRadius е удобно да тествате работата му с помощта на утилитата radclient. Например, за авторизация:

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

Или счетоводство:

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

Искам да предупредя, че подобни схеми и скриптове "без промени" не могат да се прилагат на "индустриални" мащаби. Минимумът, който се набива на очи:

  • може да се извърши "фалшификация" на MAC адреса. Достатъчно е абонатът да зададе чужд MAC и ще възникнат проблеми.
  • логиката за раздаване на гостови мрежи е под всякаква критика. Няма дори проверка "има ли клиенти с издаден такъв IP адрес вече?"

Това е просто "решение на коляно", за да работи конкретно в моите условия, не повече от това. Не съдите строго 😉

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster