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 config import app_config as config_class
|
||||||
from database import init_app
|
from database import init_app
|
||||||
|
|
||||||
import logging, os
|
import logging, os, pytz
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
|
|
||||||
# Instantiate config class
|
# Instantiate config class
|
||||||
@@ -13,8 +13,11 @@ app_config = config_class()
|
|||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object(app_config)
|
app.config.from_object(app_config)
|
||||||
|
app.config['TZ'] = pytz.timezone(app.config['APP_TIMEZONE'])
|
||||||
init_app(app)
|
init_app(app)
|
||||||
|
|
||||||
|
app.config['TZ'] = pytz.timezone(app.config['APP_TIMEZONE'])
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
if app.config.get('LOG_TO_FILE'):
|
if app.config.get('LOG_TO_FILE'):
|
||||||
log_file = app.config.get('LOG_FILE_PATH', '/app/logs/app.log')
|
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'))
|
OUI_API_DAILY_LIMIT = int(os.getenv('OUI_API_DAILY_LIMIT', '10000'))
|
||||||
|
|
||||||
# These get set in __init__
|
# These get set in __init__
|
||||||
APP_TIMEZONE = 'UTC'
|
APP_TIMEZONE = os.getenv('APP_TIMEZONE', 'UTC')
|
||||||
TZ = pytz.utc
|
TZ = pytz.timezone(APP_TIMEZONE)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
tz_name = os.getenv('APP_TIMEZONE', 'UTC')
|
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 database import get_db
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import requests, pytz
|
import requests, pytz
|
||||||
@@ -13,14 +13,14 @@ def time_ago(dt):
|
|||||||
if not dt:
|
if not dt:
|
||||||
return "n/a"
|
return "n/a"
|
||||||
|
|
||||||
tz_name = current_app.config.get('APP_TIMEZONE', 'UTC')
|
|
||||||
local_tz = current_app.config.get('TZ', pytz.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:
|
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)
|
dt = dt.astimezone(local_tz)
|
||||||
now = datetime.now(local_tz)
|
now = datetime.now(local_tz)
|
||||||
diff = now - dt
|
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:
|
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:
|
app:
|
||||||
build:
|
build:
|
||||||
context: ./app
|
context: ./app
|
||||||
@@ -16,7 +60,9 @@ services:
|
|||||||
- FLASK_ENV=production
|
- FLASK_ENV=production
|
||||||
- PYTHONPATH=/app
|
- PYTHONPATH=/app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
nginx:
|
nginx:
|
||||||
build:
|
build:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
@@ -25,4 +71,7 @@ services:
|
|||||||
- "8080:80"
|
- "8080:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- app
|
- 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