- Support both "*.domain" and ".domain" wildcard formats in haproxy_router.py - Sort wildcards by length (longest first) for correct specificity matching - Add auto-reload: check routes file mtime every 10 requests - Update metablogizerctl to use mitmproxyctl sync-routes Also fix luci-app-wazuh api.js to use baseclass.extend Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
200 lines
7.2 KiB
Python
200 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
HAProxy Backend Router for mitmproxy
|
|
Routes incoming requests from HAProxy to actual backends based on Host header
|
|
Works with secubox_analytics.py for threat detection
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
from mitmproxy import http, ctx
|
|
from mitmproxy.net.server_spec import ServerSpec
|
|
from mitmproxy.connection import Address
|
|
|
|
# Backend routing configuration file
|
|
ROUTES_FILE = "/data/haproxy-routes.json"
|
|
DEFAULT_BACKEND = ("127.0.0.1", 8081) # LuCI fallback
|
|
|
|
class HaproxyRouter:
|
|
def __init__(self):
|
|
self.routes = {}
|
|
self._routes_mtime = 0
|
|
self._check_interval = 10 # Check file every N requests
|
|
self._request_count = 0
|
|
self._load_routes()
|
|
ctx.log.info(f"HAProxy Router loaded with {len(self.routes)} routes")
|
|
|
|
def _load_routes(self):
|
|
"""Load routing table from JSON file"""
|
|
if os.path.exists(ROUTES_FILE):
|
|
try:
|
|
self._routes_mtime = os.path.getmtime(ROUTES_FILE)
|
|
with open(ROUTES_FILE, 'r') as f:
|
|
self.routes = json.load(f)
|
|
ctx.log.info(f"Loaded routes: {list(self.routes.keys())}")
|
|
except Exception as e:
|
|
ctx.log.error(f"Failed to load routes: {e}")
|
|
self.routes = {}
|
|
else:
|
|
ctx.log.warn(f"Routes file not found: {ROUTES_FILE}")
|
|
self._generate_default_routes()
|
|
|
|
def _check_reload_routes(self):
|
|
"""Check if routes file has changed and reload if needed"""
|
|
try:
|
|
if os.path.exists(ROUTES_FILE):
|
|
mtime = os.path.getmtime(ROUTES_FILE)
|
|
if mtime > self._routes_mtime:
|
|
ctx.log.info("Routes file changed, reloading...")
|
|
self._load_routes()
|
|
except Exception as e:
|
|
ctx.log.error(f"Error checking routes file: {e}")
|
|
|
|
def _generate_default_routes(self):
|
|
"""Generate default routes from UCI if available"""
|
|
self.routes = {
|
|
# Format: "hostname": ["ip", port]
|
|
"blog.cybermind.fr": ["192.168.255.1", 4000],
|
|
"devel.cybermind.fr": ["192.168.255.1", 3000],
|
|
"devel.maegia.tv": ["192.168.255.1", 3000],
|
|
"play.cybermind.fr": ["192.168.255.1", 8501],
|
|
"crt.cybermind.fr": ["192.168.255.1", 8502],
|
|
"secubox.maegia.tv": ["127.0.0.1", 8081],
|
|
"glances.maegia.tv": ["192.168.255.1", 61208],
|
|
"factory.maegia.tv": ["192.168.255.1", 7331],
|
|
"meet.maegia.tv": ["127.0.0.1", 8443],
|
|
}
|
|
# Save for next time
|
|
try:
|
|
with open(ROUTES_FILE, 'w') as f:
|
|
json.dump(self.routes, f, indent=2)
|
|
except:
|
|
pass
|
|
|
|
def _get_backend(self, host: str) -> tuple:
|
|
"""Get backend address for hostname"""
|
|
# Remove port from host if present
|
|
hostname = host.split(':')[0].lower()
|
|
|
|
# 1. Try exact match first
|
|
if hostname in self.routes:
|
|
backend = self.routes[hostname]
|
|
return (backend[0], backend[1])
|
|
|
|
# 2. Try wildcard matching - collect all wildcard patterns
|
|
# Support both "*.domain" and ".domain" formats
|
|
wildcards = []
|
|
for pattern, backend in self.routes.items():
|
|
if pattern.startswith('*.'):
|
|
# Standard wildcard: *.gk2.secubox.in
|
|
suffix = pattern[1:] # Keep the dot: .gk2.secubox.in
|
|
wildcards.append((suffix, backend))
|
|
elif pattern.startswith('.'):
|
|
# HAProxy-style wildcard: .gk2.secubox.in
|
|
suffix = pattern # Already has dot: .gk2.secubox.in
|
|
wildcards.append((suffix, backend))
|
|
|
|
# Sort by suffix length descending - longest (most specific) first
|
|
wildcards.sort(key=lambda x: len(x[0]), reverse=True)
|
|
|
|
for suffix, backend in wildcards:
|
|
if hostname.endswith(suffix):
|
|
return (backend[0], backend[1])
|
|
|
|
ctx.log.warn(f"No route for {hostname}, using default")
|
|
return DEFAULT_BACKEND
|
|
|
|
def request(self, flow: http.HTTPFlow):
|
|
"""Route request to appropriate backend"""
|
|
# Periodically check if routes file has changed
|
|
self._request_count += 1
|
|
if self._request_count >= self._check_interval:
|
|
self._request_count = 0
|
|
self._check_reload_routes()
|
|
|
|
host = flow.request.host_header or flow.request.host
|
|
backend = self._get_backend(host)
|
|
|
|
# Set the upstream server
|
|
flow.request.host = backend[0]
|
|
flow.request.port = backend[1]
|
|
|
|
# Log routing decision
|
|
ctx.log.debug(f"ROUTE: {host} -> {backend[0]}:{backend[1]}")
|
|
|
|
# Store original host for analytics
|
|
flow.metadata['original_host'] = host
|
|
flow.metadata['backend'] = f"{backend[0]}:{backend[1]}"
|
|
|
|
|
|
def generate_routes_from_uci():
|
|
"""Generate routes JSON from HAProxy UCI config"""
|
|
routes = {}
|
|
|
|
try:
|
|
# Get vhosts
|
|
result = subprocess.run(
|
|
['uci', 'show', 'haproxy'],
|
|
capture_output=True, text=True
|
|
)
|
|
|
|
vhosts = {}
|
|
backends = {}
|
|
|
|
for line in result.stdout.split('\n'):
|
|
# Parse vhost domains
|
|
if '=vhost' in line:
|
|
vhost_name = line.split('=')[0].split('.')[1]
|
|
vhosts[vhost_name] = {}
|
|
elif '.domain=' in line:
|
|
parts = line.split('=')
|
|
vhost_name = parts[0].split('.')[1]
|
|
domain = parts[1].strip("'")
|
|
if vhost_name in vhosts:
|
|
vhosts[vhost_name]['domain'] = domain
|
|
elif '.backend=' in line and '=vhost' not in line:
|
|
parts = line.split('=')
|
|
vhost_name = parts[0].split('.')[1]
|
|
backend_name = parts[1].strip("'")
|
|
if vhost_name in vhosts:
|
|
vhosts[vhost_name]['backend'] = backend_name
|
|
|
|
# Parse backend servers
|
|
if '=backend' in line:
|
|
backend_name = line.split('=')[0].split('.')[1]
|
|
backends[backend_name] = {}
|
|
elif '.server=' in line:
|
|
parts = line.split('=')
|
|
backend_name = parts[0].split('.')[1]
|
|
server_spec = parts[1].strip("'")
|
|
# Parse "name ip:port check"
|
|
server_parts = server_spec.split()
|
|
if len(server_parts) >= 2:
|
|
addr = server_parts[1]
|
|
if ':' in addr:
|
|
ip, port = addr.rsplit(':', 1)
|
|
backends[backend_name] = {'ip': ip, 'port': int(port)}
|
|
|
|
# Build routes
|
|
for vhost_name, vhost in vhosts.items():
|
|
if 'domain' in vhost and 'backend' in vhost:
|
|
backend_name = vhost['backend']
|
|
if backend_name in backends:
|
|
backend = backends[backend_name]
|
|
routes[vhost['domain']] = [backend.get('ip', '127.0.0.1'), backend.get('port', 80)]
|
|
|
|
return routes
|
|
except Exception as e:
|
|
print(f"Error generating routes: {e}")
|
|
return {}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# CLI tool to generate routes
|
|
routes = generate_routes_from_uci()
|
|
print(json.dumps(routes, indent=2))
|
|
|
|
|
|
addons = [HaproxyRouter()]
|