new radius server

This commit is contained in:
2025-04-01 17:43:52 -04:00
parent 9d4b21b5ae
commit 1482643261
13 changed files with 174 additions and 10 deletions

Binary file not shown.

View File

@@ -5,7 +5,7 @@ from views.group_views import group
from config import app_config as config_class
from database import init_app
import logging, os
import logging, os, pytz
from logging.handlers import RotatingFileHandler
# Instantiate config class
@@ -13,8 +13,11 @@ app_config = config_class()
app = Flask(__name__)
app.config.from_object(app_config)
app.config['TZ'] = pytz.timezone(app.config['APP_TIMEZONE'])
init_app(app)
app.config['TZ'] = pytz.timezone(app.config['APP_TIMEZONE'])
# Logging
if app.config.get('LOG_TO_FILE'):
log_file = app.config.get('LOG_FILE_PATH', '/app/logs/app.log')

View File

@@ -17,8 +17,8 @@ class Config:
OUI_API_DAILY_LIMIT = int(os.getenv('OUI_API_DAILY_LIMIT', '10000'))
# These get set in __init__
APP_TIMEZONE = 'UTC'
TZ = pytz.utc
APP_TIMEZONE = os.getenv('APP_TIMEZONE', 'UTC')
TZ = pytz.timezone(APP_TIMEZONE)
def __init__(self):
tz_name = os.getenv('APP_TIMEZONE', 'UTC')

View File

@@ -1,4 +1,4 @@
from flask import Blueprint, render_template, request, jsonify
from flask import Blueprint, render_template, request, jsonify, current_app
from database import get_db
from datetime import datetime
import requests, pytz
@@ -13,14 +13,14 @@ def time_ago(dt):
if not dt:
return "n/a"
tz_name = current_app.config.get('APP_TIMEZONE', 'UTC')
local_tz = current_app.config.get('TZ', pytz.utc)
# Only assign UTC tzinfo if naive
# If the DB datetime is naive, assume it's already in local server time
if dt.tzinfo is None:
dt = dt.replace(tzinfo=pytz.utc)
server_tz = pytz.timezone('America/Toronto') # Or your DB server's real timezone
dt = server_tz.localize(dt)
# Convert to app timezone
# Convert to the app's configured timezone (from .env)
dt = dt.astimezone(local_tz)
now = datetime.now(local_tz)
diff = now - dt

3
db/conf.d/custom.cnf Normal file
View File

@@ -0,0 +1,3 @@
[mysqld]
bind-address = 0.0.0.0
port = 3306

31
db/init/init-schema.sql Normal file
View File

@@ -0,0 +1,31 @@
-- init-schema.sql
-- Table for registered users (MAC-based auth)
CREATE TABLE IF NOT EXISTS users (
mac_address CHAR(12) NOT NULL PRIMARY KEY CHECK (mac_address REGEXP '^[0-9A-Fa-f]{12}$'),
description VARCHAR(200),
vlan_id VARCHAR(64)
);
-- Table for auth logs
CREATE TABLE IF NOT EXISTS auth_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
mac_address CHAR(12) NOT NULL CHECK (mac_address REGEXP '^[0-9A-Fa-f]{12}$'),
reply ENUM('Access-Accept', 'Access-Reject') NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Table for MAC vendor caching
CREATE TABLE IF NOT EXISTS mac_vendors (
mac_prefix CHAR(6) NOT NULL PRIMARY KEY CHECK (mac_prefix REGEXP '^[0-9A-Fa-f]{6}$'),
vendor_name VARCHAR(255),
status ENUM('found', 'not_found') DEFAULT 'found',
last_checked DATETIME DEFAULT CURRENT_TIMESTAMP,
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Table for VLAN groups
CREATE TABLE IF NOT EXISTS groups (
vlan_id VARCHAR(64) NOT NULL PRIMARY KEY,
description VARCHAR(200)
);

View File

@@ -1,6 +1,50 @@
---
services:
db:
image: mariadb:11
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: radius
MYSQL_USER: radiususer
MYSQL_PASSWORD: radiuspass
volumes:
- db_data:/var/lib/mysql
- ./db/conf.d:/etc/mysql/conf.d
- ./db/init:/docker-entrypoint-initdb.d
ports:
- "3306:3306" # Exposed for dev access
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
start_period: 10s
interval: 10s
timeout: 5s
retries: 3
services:
radius:
build:
context: ./radius
dockerfile: Dockerfile
depends_on:
db:
condition: service_healthy
env_file:
- .env
ports:
- "1812:1812/udp"
restart: unless-stopped
adminer:
image: adminer
restart: unless-stopped
ports:
- "8081:8080" # Access at http://localhost:8081
app:
build:
context: ./app
@@ -16,7 +60,9 @@ services:
- FLASK_ENV=production
- PYTHONPATH=/app
restart: unless-stopped
depends_on:
db:
condition: service_healthy
nginx:
build:
context: ./nginx
@@ -25,4 +71,7 @@ services:
- "8080:80"
depends_on:
- app
restart: unless-stopped
restart: unless-stopped
volumes:
db_data:

22
radius/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM python:3.9-slim
# Set working directory
WORKDIR /app
# Install runtime dependencies (for mysql-connector and networking tools)
RUN apt-get update && \
apt-get install -y gcc libmariadb-dev iputils-ping && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy RADIUS service source code
COPY . .
# Expose RADIUS port (UDP)
EXPOSE 1812/udp
# Run the RADIUS service
CMD ["python", "main.py"]

4
radius/dictionary Normal file
View File

@@ -0,0 +1,4 @@
ATTRIBUTE User-Name 1 string
ATTRIBUTE Tunnel-Type 64 integer
ATTRIBUTE Tunnel-Medium-Type 65 integer
ATTRIBUTE Tunnel-Private-Group-Id 81 string

50
radius/main.py Normal file
View File

@@ -0,0 +1,50 @@
from pyrad.server import Server, RemoteHost
from pyrad.dictionary import Dictionary
from pyrad.packet import AccessAccept, AccessReject
import mysql.connector
import os
DEFAULT_VLAN_ID = os.getenv("DEFAULT_VLAN", "999")
class MacRadiusServer(Server):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.db = mysql.connector.connect(
host=os.getenv('DB_HOST'),
port=int(os.getenv('DB_PORT', 3306)),
user=os.getenv('DB_USER'),
password=os.getenv('DB_PASSWORD'),
database=os.getenv('DB_NAME'),
)
def HandleAuthPacket(self, pkt):
username = pkt['User-Name'][0].upper()
cursor = self.db.cursor(dictionary=True)
cursor.execute("SELECT vlan_id FROM users WHERE mac_address = %s", (username,))
result = cursor.fetchone()
cursor.close()
if result:
reply = self.CreateReplyPacket(pkt)
reply.code = AccessAccept
reply.AddAttribute("Tunnel-Type", 13)
reply.AddAttribute("Tunnel-Medium-Type", 6)
reply.AddAttribute("Tunnel-Private-Group-Id", result['vlan_id'])
else:
# Fallback to default VLAN
reply = self.CreateReplyPacket(pkt)
reply.code = AccessAccept
reply["Tunnel-Type"] = 13 # VLAN
reply["Tunnel-Medium-Type"] = 6 # IEEE-802
reply["Tunnel-Private-Group-Id"] = DEFAULT_VLAN_ID
self.SendReplyPacket(pkt.fd, reply)
print(f"[INFO] MAC {mac} not found — assigned to fallback VLAN {DEFAULT_VLAN_ID}")
self.SendReplyPacket(pkt.fd, reply)
if __name__ == '__main__':
srv = MacRadiusServer(dict=Dictionary("dictionary"))
srv.hosts["0.0.0.0"] = RemoteHost("0.0.0.0", os.getenv("RADIUS_SECRET", "testing123").encode(), "localhost")
srv.BindToAddress("0.0.0.0")
srv.Run()

2
radius/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
pyrad
mysql-connector-python