new radius server
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
Binary file not shown.
@@ -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
3
db/conf.d/custom.cnf
Normal file
@@ -0,0 +1,3 @@
|
||||
[mysqld]
|
||||
bind-address = 0.0.0.0
|
||||
port = 3306
|
||||
31
db/init/init-schema.sql
Normal file
31
db/init/init-schema.sql
Normal 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)
|
||||
);
|
||||
@@ -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
22
radius/Dockerfile
Normal 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
4
radius/dictionary
Normal 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
50
radius/main.py
Normal 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
2
radius/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pyrad
|
||||
mysql-connector-python
|
||||
Reference in New Issue
Block a user