
Opgaven er at sørge for, at IP-adresser udleveres til abonnenterne. Betingelser for opgaven:
- Vi kan ikke give en separat server til autentificering — du må klare dig 😉
- Abonnenterne skal modtage netværksindstillinger via DHCP
- Netværket er heterogent. Det inkluderer både PON-udstyr og almindelige switches med konfigureret Option 82 samt WiFi-baser med access points.
- Hvis ingen af betingelserne for IP-udlevering dækkes, skal der udleveres en IP fra "gæstenetværket".
Det gode er, at der faktisk findes server på FreeBSD, som kan "arbejde", men det er "langt væk" ;), ikke "lige i dette netværk".
Der er også en fremragende enhed fra Mikrotik. Den generelle netværksdiagram ser cirka sådan ud:

Efter lidt overvejelse blev beslutningen truffet om at bruge FreeRadius til at udlevere netværksindstillinger til abonnenterne. Grundlæggende er diagrammet almindeligt: på Microtik aktiverer vi DHCP-serveren og gør den til Radius-klient. Vi konfigurerer forbindelsen DHCP-server -> Radius-klient -> Radius-server.
Det virker ikke så svært. Men! Problemet ligger i detaljerne. Nemlig:
- Ved autentificering af PON OLT i dette skema modtager FreeRadius en anmodning med User-Name lig med MAC-adressen på hovedstationen, Agent-Circuit-Id lig med MAC PON Onu og et tomt adgangskodefelt.
- Ved autentificering fra switches med option 82 kommer der en anmodning til FreeRadius med et tomt User-Name lig med MAC-adressen på abonnentens enhed og med udfyldte ekstra attributter Agent-Circuit-Id og Agent-Remote-Id, som henholdsvis indeholder MAC-adressen på relay-switch og porten, hvor abonnenten er tilsluttet.
- Nogle abonnenter med WiFi-punkter autentificeres via PAP-CHAP-protokoller.
- Nogle abonnenter med WiFi-punkter autentificeres med User-Name lig med MAC-adressen på WiFi-punktet, uden adgangskode.
Historisk kontekst: hvad er "Option 82" i DHCP?
Det er ekstra indstillinger i DHCP-protokollen, som gør det muligt at overføre yderligere information, for eksempel i felterne Agent-Circuit-Id og Agent-Remote-Id. Det bruges normalt til at overføre MAC-adressen på relay-switch og porten, hvor abonnenten er tilsluttet. I tilfælde af PON-udstyr eller WiFi-baser, giver Agent-Circuit-Id ikke nyttig information (ingen abonnentport). Den overordnede arbejdsstruktur for DHCP i dette tilfælde er som følger:

Trinvist fungerer denne ordning sådan:
- Abonnentudstyret sender en broadcast DHCP-anmodning for at modtage netværksindstillinger.
- Enheden (f.eks. switch, WiFi-basisstation eller PON), som det abonnentudstyr, der er direkte tilsluttet, "fanger" denne pakke og ændrer den ved at indlægge ekstra muligheder Option 82 og Relay agent IP address, og sender den videre i netværket.
- DHCP-serveren modtager forespørgslen, udformer et svar og sender det til det relæende udstyr.
- Det relæende udstyr videresender svarpakken til abonnentudstyret.
Det er selvfølgelig ikke så enkelt, der kræves en passende konfiguration af netværksudstyret.
Installation af FreeRadius
Med konfigurationsindstillingerne for FreeRadius er det muligt at opnå alt dette, men det er svært og ikke klart... især når man kaster sig ind i det efter N måneder, når "alt fungerer". Derfor blev det besluttet at skrive sin egen autoriseringsmodul til FreeRadius i Python. Vi vil hente data til autorisation fra MySQL-databasen. Det giver ikke mening at beskrive strukturen, da alle sammen alligevel vil lave den "til deres behov". I særdeleshed har jeg taget den struktur, der foreslås med SQL-modulet til FreeRadius, og ændret den lidt ved at tilføje felterne mac og port for hver abonnent, udover login og adgangskode.
Så lad os først installere FreeRadius:
cd /usr/ports/net/freeradius3
make config
make
install clean
I indstillingerne markerer vi for installation:

Vi laver et symlink til Python-modulet (dvs. "aktiverer" det):
ln -s /usr/local/etc/raddb/mods-available/python /usr/local/etc/raddb/mods-enabledLad os installere en ekstra modul til Python:
pip install mysql-connectorI indstillingerne for Python-modulet til FreeRadius skal vi angive stierne til modsøgning i variablen python_path. For eksempel er det hos mig:
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"
Stierne kan findes ved at starte Python-interpretatoren og indtaste kommandoerne:
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']
>Hvis dette skridt ikke tages, vil scripts skrevet i python og kørt af FreeRadius ikke finde de moduler, der er nævnt i import. Derudover er det nødvendigt at afkommentere indstillingerne for modulkald af autorisation og regnskab. For eksempel ser mit modul sådan ud:
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}
}Scriptet work.py (og alle de andre) skal placeres i /usr/local/etc/raddb/mods-config/python. I alt har jeg fået tre scripts.
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 af koden, forsøger vi med alle tilgængelige midler at identificere abonnenten ud fra hans kendte MAC-adresser eller Option 82-forbindelsen, og hvis det ikke er muligt, så tildeler vi den ældste IP-adresse, der nogensinde er brugt, fra "gæstesnetværket". Det er nu tid til at konfigurere scriptet default i mappen sites-enabled, så de nødvendige funktioner fra python-scriptet kaldes på de rigtige tidspunkter. Det er faktisk tilstrækkeligt at tilpasse filen til:
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
}
}
Lad os prøve at starte det og se, hvad der kommer i debugloggen:
/usr/local/etc/rc.d/radiusd debugHvad angår det. Når man konfigurerer FreeRadius, er det praktisk at teste dens funktion med værktøjet radclient. For eksempel autorisation:
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 testing123Eller kontingent:
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 testing123Jeg vil advare om, at anvendelse af en sådan skema og scripts "uden ændringer" i "industriel" skala bestemt ikke er tilladt. I det mindste bemærker man:
- der kan ske "forfalskning" af MAC-adressen. Det er tilstrækkeligt, at abonnenten registrerer en andens MAC, og der vil opstå problemer
- logikken for tildeling af gæstesnet er under al kritik. Der er ikke engang en kontrol: "kan der allerede være klienter med den tildelte IP-adresse?"
Det er bare en "løsning på knæene" for at fungere specifikt i mine betingelser, ikke mere end det. Døm ikke for hårdt 😉
Kilde: habr.com
