Netzwerkeinstellungen von FreeRadius über DHCP.

Netzwerkeinstellungen von FreeRadius über DHCP.
Die Aufgabe, IP-Adressen an die Kunden zu vergeben, ist eingetroffen. Die Aufgabenstellung lautet:

  • Wir stellen kein separates Server für die Authentifizierung bereit — Sie werden schon zurechtkommen 😉
  • Die Kunden sollten die Netzwerkeinstellungen über DHCP erhalten.
  • Das Netzwerk ist heterogen. Dazu gehören PON-Ausrüstung, normale Switches mit aktivierter Option 82 und WiFi-Basen mit Zugangspunkten.
  • Wenn keine der Bedingungen für die Vergabe von IP-Adressen zutrifft, muss eine IP aus dem "Gastnetzwerk" vergeben werden.

Von den positiven Aspekten: es gibt tatsächlich Server auf FreeBSD, das „arbeiten“ kann, aber es ist „lichtjahre entfernt“ ;), nicht „direkt in diesem Netzwerk“.

Es gibt auch ein hervorragendes Gerät von Mikrotik. Das allgemeine Netzwerkdiagramm sieht ungefähr so aus:

Netzwerkeinstellungen von FreeRadius über DHCP.

Nach einigem Überlegen wurde entschieden, FreeRadius für die Vergabe der Netzwerkeinstellungen an die Kunden zu verwenden. Im Prinzip ist das Schema gewöhnlich: Wir aktivieren den DHCP-Server auf Microtick und den Radius-Client dort ebenfalls. Wir konfigurieren die Verbindung DHCP-Server -> Radius-Client -> Radius-Server.

Das scheint nicht kompliziert zu sein. Aber! Der Teufel steckt im Detail. Und zwar:

  • Bei der Authentifizierung von PON OLT über dieses Schema kommt bei FreeRadius eine Anfrage mit User-Name gleich der MAC-Adresse der Hauptstation, Agent-Circuit-Id gleich der MAC des PON Onu und leerem Passwort an.
  • Bei der Authentifizierung von Switches mit Option 82 kommt bei FreeRadius eine Anfrage mit leerem User-Name gleich der MAC des Geräten des Abonnenten und ausgefüllten zusätzlichen Attributen Agent-Circuit-Id und Agent-Remote-Id, die entsprechend wiederum die MAC des Relay-Switches und den Port, an den der Abonnent angeschlossen ist, enthalten.
  • Ein Teil der Kunden an den WiFi-Punkten authentifiziert sich über die PAP-CHAP-Protokolle.
  • Ein Teil der Kunden an den WiFi-Punkten authentifiziert sich mit User-Name gleich der MAC-Adresse des WiFi-Punkts, ohne Passwort.

Historischer Hintergrund: Was ist „Option 82“ bei DHCP?

Das sind zusätzliche Optionen im DHCP-Protokoll, die es ermöglichen, zusätzliche Informationen zu übermitteln, beispielsweise in den Feldern Agent-Circuit-Id und Agent-Remote-Id. Diese werden normalerweise verwendet, um die MAC-Adresse des Relay-Switches und den Port, an den der Abonnent angeschlossen ist, zu übermitteln. Im Falle von PON-Ausrüstung oder WiFi-Basen bringt das Feld Agent-Circuit-Id keine nützlichen Informationen (kein Port des Abonnenten). In diesem Zusammenhang sieht das allgemeine Arbeitschema von DHCP folgendermaßen aus:

Netzwerkeinstellungen von FreeRadius über DHCP.

Schrittweise funktioniert dieses Schema so:

  1. Die Abonnentenausrüstung sendet eine Broadcast-DHCP-Anfrage zur Erlangung von Netzwerkeinstellungen.
  2. Das Gerät (zum Beispiel ein Switch, ein WiFi-Basisstation oder PON), das direkt mit der Endgeräteausrüstung verbunden ist, "fängt" dieses Paket ab und verändert es, indem es zusätzliche Optionen wie Option 82 und Relay-Agent-IP-Adresse einfügt und es weiter im Netzwerk überträgt.
  3. Der DHCP-Server empfängt die Anfrage, erstellt eine Antwort und sendet sie an das Relay-Gerät.
  4. Das Relay-Gerät leitet das Antwortpaket an das Endgerät weiter.

So einfach funktioniert das natürlich nicht, es ist eine entsprechende Konfiguration der Netzwerkausrüstung erforderlich.

Installation von FreeRadius

Durch die Konfigurationseinstellungen von FreeRadius kann man dies alles erreichen, allerdings ist es kompliziert und unklar… besonders wenn man nach N Monaten dort hineinschaut, wenn "alles funktioniert". Daher wurde beschlossen, ein eigenes Authentifizierungsmodul für FreeRadius in Python zu schreiben. Die Daten zur Authentifizierung werden aus einer MySQL-Datenbank entnommen. Es hat keinen Sinn, die Struktur zu beschreiben, da sowieso jeder sie "auf sich zuschneidet". Ich habe beispielsweise die Struktur verwendet, die mit dem SQL-Modul für FreeRadius angeboten wird, und sie leicht geändert, indem ich das Feld mac und port für jeden Benutzer hinzugefügt habe, zusätzlich zu Benutzername und Passwort.

Zunächst installieren wir FreeRadius:

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

In den Einstellungen markieren wir für die Installation:

Netzwerkeinstellungen von FreeRadius über DHCP.

Wir erstellen einen Symlink für das Python-Modul (d.h. wir "aktivieren" es):

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

Wir installieren ein zusätzliches Modul für Python:

pip install mysql-connector

In den Einstellungen des Python-Moduls für FreeRadius müssen die Suchpfade für Module in die Variable python_path eingetragen werden. Bei mir ist das beispielsweise:

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"

Die Pfade kann man herausfinden, indem man den Python-Interpreter startet und die Befehle eingibt:

root@phaeton:/usr/local/etc/raddb/mods-enabled# python
Python 2.7.15 (default, 8. Dez 2018, 01:22:25) 
[GCC 4.2.1 Kompatibel mit FreeBSD Clang 6.0.1 (tags/RELEASE_601/final 335540)] auf freebsd12
Geben Sie "help", "copyright", "credits" oder "license" für weitere Informationen ein.
>>> 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']
>

Wenn dieser Schritt nicht unternommen wird, finden die mit Python geschriebenen Skripte, die FreeRadius gestartet haben, die in Import aufgelisteten Module nicht. Außerdem müssen in den Konfigurationseinstellungen des Moduls die Funktionen zur Authentifizierung und Abrechnung wieder aktiviert werden. Zum Beispiel sieht mein Modul so aus:

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}

}

Das Skript work.py (und alle anderen) muss in /usr/local/etc/raddb/mods-config/python abgelegt werden. Insgesamt habe ich drei 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

Wie aus dem Code zu erkennen ist, versuchen wir auf jede mögliche Weise, den Nutzer anhand seiner bekannten MAC-Adressen oder der Option 82 zu identifizieren. Falls dies nicht gelingt, geben wir die älteste jemals verwendete IP-Adresse aus dem 'Gast'-Netzwerk aus. Es bleibt, das Standard-Skript im Ordner sites-enabled zu konfigurieren, damit die erforderlichen Funktionen aus dem Python-Skript zu den festgelegten Zeitpunkten aufgerufen werden. Es reicht praktisch aus, die Datei in folgendes Format zu bringen:

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

Wir versuchen, es zu starten und sehen, was im Debug-Log ankommt:

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

Was noch. Bei der Konfiguration von FreeRadius ist es praktisch, seine Funktionsweise mit dem Tool radclient zu testen. Zum Beispiel die Authentifizierung:

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

Oder Accounting:

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

Ich möchte darauf hinweisen, dass es in „industriellem“ Maßstab nicht akzeptabel ist, solche Schemata und Skripte „unverändert“ anzuwenden. Mindestens fallen auf:

  • die Möglichkeit der „Fälschung“ von MAC-Adressen. Der Abonnent kann sich ganz einfach eine fremde MAC-Adresse zuweisen und es gibt Probleme.
  • Die Logik der Vergabe von Gastnetzwerken ist unter aller Kritik. Es gibt nicht einmal eine Überprüfung: „Hat nicht vielleicht bereits jemand einen solchen IP-Adresse zugewiesen bekommen?“

Das ist einfach eine „Notlösung“, die konkret unter meinen Bedingungen funktioniert, nicht mehr. Urteil nicht zu hart 😉

Quelle: habr.com

Zuverlässiges Hosting für Websites mit DDoS-Schutz kaufen, VPS VDS Server 🔥 Zuverlässiges Hosting für Websites mit DDoS-Schutz kaufen, VPS VDS Server - ProHoster