Netzwerkeinstellungen von FreeRadius über DHCP

Netzwerkeinstellungen von FreeRadius über DHCP
Die Aufgabe besteht darin, die Vergabe von IP-Adressen an die Teilnehmer einzurichten. Bedingungen der Aufgabe:

  • Wir stellen keinen separaten Server für die Authentifizierung bereit – damit müssen Sie auskommen 😉
  • Die Teilnehmer müssen ihre Netzwerkeinstellungen über DHCP erhalten.
  • Das Netzwerk ist heterogen. Dazu gehören sowohl PON-Geräte als auch herkömmliche Switches mit konfigurierter Option 82 sowie WLAN-Basen mit Zugangs­punkten.
  • Falls keine der Bedingungen für die IP-Vergabe zutrifft, muss eine IP aus dem 'Gast'-Netzwerk zugewiesen werden.

Gute Nachrichten: Es gibt tatsächlich der Server eine FreeBSD-Instanz, die 'arbeiten' kann, aber sie ist 'in weiter Ferne' ;), 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 ein wenig Überlegung wurde beschlossen, für die Zuweisung von Netzwerkeinstellungen an die Teilnehmer FreeRadius zu verwenden. Im Prinzip handelt es sich um ein gewöhnliches Schema: Auf dem Mikrotik aktivieren wir den DHCP-Server und gleichzeitig den Radius-Client. 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 des PON OLT kommt über dieses Schema eine Anfrage an FreeRadius mit dem User-Name, der der MAC-Adresse der Hauptstation entspricht, Agent-Circuit-Id, die der MAC der PON ONU entspricht, und einem leeren Passwort.
  • Beim Anmelden von Switches mit Option 82 wird an FreeRadius eine Anfrage mit einem leeren User-Name gesendet, der der MAC-Adresse des Endgeräts entspricht, sowie ausgefüllten zusätzlichen Attributen Agent-Circuit-Id und Agent-Remote-Id, die jeweils wieder die MAC-Adresse des Relay-Switches und den Port enthalten, an den der Endbenutzer angeschlossen ist.
  • Ein Teil der Benutzer mit WiFi-Punkten authentifiziert sich über die Protokolle PAP-CHAP.
  • Ein Teil der Benutzer mit WiFi-Punkten authentifiziert sich mit einem User-Name, der der MAC-Adresse des WiFi-Punkts entspricht, ohne Passwort.

Historischer Hintergrund: Was ist «Option 82» im DHCP?

Dies 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. Sie werden normalerweise verwendet, um die MAC-Adresse des Relay-Switches und den Port, an den der Benutzer angeschlossen ist, zu übertragen. Bei PON-Geräten oder WiFi-Basisstationen enthält das Feld Agent-Circuit-Id keine nützlichen Informationen (kein Benutzerport). Das allgemeine Funktionsschema von DHCP sieht in diesem Fall folgendermaßen aus:

Netzwerkeinstellungen von FreeRadius über DHCP

Schritt-für-Schritt funktioniert dieses Schema so:

  1. Die Endgerätetechnik sendet eine Broadcast-DHCP-Anfrage zur Abholung der Netzwerkeinstellungen.
  2. Das Gerät (zum Beispiel ein Switch, ein WiFi-Basisstation oder ein PON), an das die Endgeräte direkt angeschlossen sind, "fängt" dieses Paket und modifiziert es, indem es zusätzliche Optionen wie Option 82 und die Relay-Agent-IP-Adresse einfügt, und leitet es weiter im Netzwerk.
  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

Mit den Konfigurationseinstellungen von FreeRadius kann man all dies natürlich erreichen, aber es ist kompliziert und unklar... besonders wenn man nach N Monaten, in denen "alles funktioniert", dort hineinschaut. Daher wurde beschlossen, ein eigenes Authentifizierungsmodul für FreeRadius in Python zu schreiben. Die Authentifizierungsdaten werden wir aus einer MySQL-Datenbank beziehen. Es hat keinen Sinn, die Struktur zu beschreiben, da jeder sie sowieso "nach seinen Wünschen" gestalten wird. Ich habe beispielsweise die Struktur, die mit dem SQL-Modul für FreeRadius angeboten wird, übernommen und ein wenig angepasst, indem ich ein Feld für die MAC-Adresse und den Port für jeden Benutzer hinzugefügt habe, zusätzlich zu Benutzername und Passwort.

Fangen wir also an, FreeRadius zu installieren:

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

In den Einstellungen aktivieren wir die Installation von:

Netzwerkeinstellungen von FreeRadius über DHCP

Wir erstellen einen Symlink für das Modul python (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 der Module in die Variable python_path eingetragen werden. Bei mir sieht das so aus:

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 können ermittelt werden, indem man den Python-Interpreter startet und die folgenden Befehle eingibt:

root@phaeton:/usr/local/etc/raddb/mods-enabled# python
Python 2.7.15 (standardmäßig, 8. Dez 2018, 01:22:25) 
[GCC 4.2.1 kompatibles FreeBSD Clang 6.0.1 (tags/RELEASE_601/final 335540)] auf freebsd12
Geben Sie "help", "copyright", "credits" oder "license" ein, um mehr Informationen zu erhalten.
>>> 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 auf Python geschriebenen Skripte, die in FreeRadius ausgeführt werden, die im Import aufgeführten Module nicht. Zudem müssen in den Einstellungen des Moduls die Funktionen für die Autorisierung und das Accounting aktiviert werden. Mein Modul sieht zum Beispiel 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 gelegt werden. Insgesamt habe ich drei Skripte erstellt.

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 im Code ersichtlich, versuchen wir auf alle verfügbaren Arten, den Teilnehmer anhand seiner bekannten MAC-Adressen oder der Option 82 zu identifizieren. Wenn dies nicht gelingt, vergeben wir die älteste jemals verwendete IP-Adresse aus dem 'Gästenetzwerk'. Es bleibt, das Standard-Skript im Verzeichnis sites-enabled so einzustellen, dass die benötigten Funktionen des Skripts in Python zu den festgelegten Zeitpunkten aufgerufen werden. Faktisch muss die Datei nur folgendermaßen aussehen:

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 erscheint:

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

Außerdem ist es beim Einrichten von FreeRadius praktisch, dessen Funktionalität 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 Abrechnung:

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 man ein solches Schema und diese Skripte „ohne Änderungen“ in „industriellem“ Maßstab keinesfalls verwenden kann. Mindestens fällt auf:

  • die Möglichkeit der „Fälschung“ von MAC-Adressen. Es reicht, wenn der Kunde sich eine fremde MAC-Adresse einträgt, und es treten Probleme auf.
  • Die Logik der Vergabe von Gastnetzwerken ist unter jeder Kritik. Es gibt nicht einmal eine Überprüfung: „Gibt es möglicherweise bereits Kunden mit einer solchen IP-Adresse?“

Das ist einfach eine „Notlösung“, die spezifisch unter meinen Bedingungen funktioniert, nicht mehr. Bitte urteilen Sie nachsichtig 😉

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster