
De taak is om de toewijzing van IP-adressen aan abonnees in te regelen. Voorwaarden van de taak:
- We geven geen aparte server voor authenticatie — je komt er wel zonder 😉
- Abonnees moeten netwerkinstellingen ontvangen via DHCP
- Het netwerk is divers. Dit omvat zowel PON-apparatuur als gewone switches met ingeschakelde Optie 82 en WiFi-bases met punten.
- Als geen van de voorwaarden voor het toewijzen van IP-adressen van toepassing is, moet er een IP uit het 'gast' netwerk worden uitgegeven.
Het goede nieuws is: er is er toch één server op FreeBSD, dat kan 'werken', maar het is 'ver weg';), niet 'rechtstreeks in dit netwerk'.
Er is ook een geweldig apparaat van Mikrotik. Het algemene netwerkdiagram is ongeveer als volgt:

Na wat nadenken is besloten om FreeRadius te gebruiken voor het verstrekken van netwerkinstellingen aan abonnees. In principe is het schema gebruikelijk: op de Mikrotik schakelen we de DHCP-server in, en ook de Radius Client. We configureren de koppeling DHCP-server -> Radius Client -> Radius-server.
Het lijkt niet moeilijk. Maar! De duivel zit in de details. Namelijk:
- Bij de authenticatie van PON OLT volgens dit schema, komt er een verzoek naar FreeRadius met een User-Name gelijk aan het MAC-adres van het hoofdstation, Agent-Circuit-Id gelijk aan het MAC van PON ONU en een leeg wachtwoord.
- Bij authenticatie met switches met optie 82, komt er een verzoek naar FreeRadius met een leeg User-Name gelijk aan het MAC-adres van het abonnementsapparaat en ingevulde extra attributen Agent-Circuit-Id en Agent-Remote-Id die respectievelijk ook het MAC-adres van de relais-switch en de poort bevatten waaraan de abonnee is aangesloten.
- Een deel van de abonnees met WiFi-punten authenticeren via PAP-CHAP protocollen
- Een deel van de abonnees met WiFi-punten authenticeren met een User-Name gelijk aan het MAC-adres van de WiFi-punt, zonder wachtwoord.
Historische achtergrond: wat is 'Optie 82' bij DHCP
Dit zijn extra opties van het DHCP-protocol die het mogelijk maken om extra informatie door te geven, bijvoorbeeld in de velden Agent-Circuit-Id en Agent-Remote-Id. Dit wordt meestal gebruikt voor het doorgeven van het MAC-adres van de relais-switch en de poort waaraan de abonnee is aangesloten. Bij PON-apparatuur of basisstations van WiFi bevat het veld Agent-Circuit-Id geen nuttige informatie (er is geen poort voor de abonnee). Het algemene schema van DHCP werkt in dit geval als volgt:

Stap voor stap werkt dit schema als volgt:
- De abonnee-apparatuur doet een broadcast DHCP-verzoek voor het verkrijgen van netwerkinstellingen.
- Een apparaat (bijvoorbeeld een switch, WiFi basisstation of PON) waaraan de klantapparatuur rechtstreeks is aangesloten, 'vangt' dit pakket en past het aan door extra opties zoals Option 82 en Relay agent IP-adres toe te voegen, en verstuurt het verder door het netwerk.
- De DHCP-server ontvangt het verzoek, stelt een antwoord op en stuurt dit naar het relay-apparaat.
- Het relay-apparaat stuurt het antwoordpakket door naar de klantapparatuur.
Het werkt natuurlijk niet zo eenvoudig; er is een juiste configuratie van de netwerkinfrastructuur nodig.
Installatie van FreeRadius
Met de configuratie-instellingen van FreeRadius kan je dit uiteraard bereiken, maar het is moeilijk en onduidelijk... vooral wanneer je er na maanden 'alles werkt' weer induikt. Daarom is besloten om een eigen authenticatiemodule voor FreeRadius te schrijven in Python. We zullen de gegevens voor authenticatie uit een MySQL-database halen. Het heeft geen zin om de structuur te beschrijven; iedereen zal die toch 'op zijn eigen manier' maken. Ik heb specifiek de structuur genomen die met de SQL-module voor FreeRadius wordt aangeboden en deze iets aangepast door een veld voor mac en port toe te voegen voor elke abonnee, naast gebruikersnaam-wachtwoord.
Laten we beginnen met de installatie van FreeRadius:
cd /usr/ports/net/freeradius3
make config
make
install clean
In de instellingen kiezen we voor de installatie:

Maak een symlink naar de python-module (d.w.z. 'schakel' deze in):
ln -s /usr/local/etc/raddb/mods-available/python /usr/local/etc/raddb/mods-enabledLaten we een extra module voor python installeren:
pip install mysql-connectorIn de instellingen van de python-module voor FreeRadius moeten de paden voor modulezoekopdrachten in de variabele python_path worden opgegeven. Bijvoorbeeld, bij mij is dat:
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"
De paden kunnen worden verkregen door de python-interpreter te starten en de opdrachten in te voeren:
root@phaeton:/usr/local/etc/raddb/mods-enabled# python
Python 2.7.15 (default, 8 dec 2018, 01:22:25)
[GCC 4.2.1 Compatible FreeBSD Clang 6.0.1 (tags/RELEASE_601/final 335540)] op freebsd12
Typ "help", "copyright", "credits" of "license" voor meer informatie.
>>> 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']
>Als je deze stap niet neemt, zullen de scripts geschreven in Python en uitgevoerd door FreeRadius de modules die in de import zijn vermeld, niet kunnen vinden. Daarnaast moet je de functies voor autorisatie en accounting in de module-instellingen uitcommentariëren. Mijn module ziet er als volgt uit:
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}
}Het script work.py (en alle andere) moet worden geplaatst in /usr/local/etc/raddb/mods-config/python. In totaal heb ik drie 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
Zoals te zien is in de code, proberen we de abonnee op alle mogelijke manieren te identificeren aan de hand van zijn bekende MAC-adressen of de combinatie van Option 82, en als dat niet lukt, geven we het oudste IP-adres dat ooit is gebruikt in het "gasten" netwerk. We moeten het script default in de map sites-enabled configureren, zodat de benodigde functies uit het Python-script op de juiste momenten worden aangeroepen. In feite is het voldoende om het bestand in de volgende vorm te brengen:
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
}
}
Laten we het proberen uit te voeren en kijken wat er in het foutopsporingslog verschijnt:
/usr/local/etc/rc.d/radiusd debugWat nog meer. Bij het instellen van FreeRadius is het handig om zijn werking te testen met het hulpprogramma radclient. Bijvoorbeeld autorisatie:
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 testing123Of accountancy:
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 testing123Ik wil waarschuwen dat het toepassen van dergelijke schema's en scripts ‘zonder wijzigingen’ op ‘industriële’ schaal absoluut niet kan. Ten minste vallen op:
- het is mogelijk om het MAC-adres te ‘falsificeren’. Het is voldoende voor de abonnee om een vreemd MAC-adres in te stellen en er zullen problemen zijn.
- de logica voor het toewijzen van guest-netwerken is beneden alle kritiek. Er is zelfs geen controle of ‘zijn er mogelijk al klanten met een toegewezen IP-adres?’
Dit is gewoon een ‘oplossing uit de losse pols’, zodat het specifiek onder mijn voorwaarden werkt, niet meer dan dat. Beoordeel niet te hard 😉
Bron: habr.com
