mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 13:59:40 +00:00
feat(geoip): Use local MaxMind database for offline GeoIP lookups
- WAF API: Replace ipapi.co HTTP calls with local GeoLite2-Country.mmdb - CrowdSec frontend: Use backend /api/v1/waf/geoip/ instead of ipapi.co - nginx: Add routes for /system/ and /api/v1/system/ - Avoids CORS issues and rate limiting from remote GeoIP services Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
25d2f87913
commit
38a3a45f19
58
common/nginx/webui.conf
Normal file
58
common/nginx/webui.conf
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
server {
|
||||
listen 0.0.0.0:9080;
|
||||
root /usr/share/secubox/www;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8001;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location /shared/ {
|
||||
alias /usr/share/secubox/www/shared/;
|
||||
}
|
||||
|
||||
# CrowdSec Dashboard static files
|
||||
location /crowdsec/ {
|
||||
alias /usr/share/secubox/www/crowdsec/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /crowdsec/index.html;
|
||||
}
|
||||
|
||||
# CrowdSec API proxy
|
||||
location /api/v1/crowdsec/ {
|
||||
proxy_pass http://unix:/run/secubox/crowdsec.sock:/;
|
||||
include /etc/nginx/snippets/secubox-proxy.conf;
|
||||
}
|
||||
|
||||
# WAF Dashboard static files
|
||||
location /waf/ {
|
||||
alias /usr/share/secubox/www/waf/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /waf/index.html;
|
||||
}
|
||||
|
||||
# WAF API proxy
|
||||
location /api/v1/waf/ {
|
||||
proxy_pass http://unix:/run/secubox/waf.sock:/;
|
||||
include /etc/nginx/snippets/secubox-proxy.conf;
|
||||
}
|
||||
|
||||
# System Hub Dashboard static files
|
||||
location /system/ {
|
||||
alias /usr/share/secubox/www/system/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /system/index.html;
|
||||
}
|
||||
|
||||
# System Hub API proxy
|
||||
location /api/v1/system/ {
|
||||
proxy_pass http://unix:/run/secubox/system.sock:/;
|
||||
include /etc/nginx/snippets/secubox-proxy.conf;
|
||||
}
|
||||
}
|
||||
|
|
@ -246,10 +246,10 @@
|
|||
|
||||
// Use HTTPS GeoIP service with fallback
|
||||
try {
|
||||
// Primary: ipapi.co (free, HTTPS, 1000/day limit)
|
||||
const res = await fetch('https://ipapi.co/' + ip + '/country/', { mode: 'cors' });
|
||||
// Backend GeoIP API (local MaxMind database)
|
||||
const res = await fetch('/api/v1/waf/geoip/' + ip);
|
||||
if (res.ok) {
|
||||
const cc = (await res.text()).trim();
|
||||
const data = await res.json(); const cc = data.country || "";
|
||||
if (cc.length === 2) {
|
||||
setGeoCache(ip, cc);
|
||||
return cc;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ from fastapi import FastAPI, Depends, HTTPException, Request
|
|||
from pydantic import BaseModel
|
||||
from secubox_core.auth import require_jwt
|
||||
from secubox_core.config import get_config
|
||||
import geoip2.database
|
||||
import geoip2.errors
|
||||
|
||||
app = FastAPI(title="SecuBox WAF")
|
||||
|
||||
|
|
@ -506,3 +508,49 @@ async def update_whitelist(req: WhitelistRequest):
|
|||
"""Add or remove IP from whitelist."""
|
||||
# In production, this would update the TOML config file
|
||||
return {"success": True, "ip": req.ip, "action": req.action}
|
||||
|
||||
# GeoIP lookup with caching
|
||||
import urllib.request
|
||||
import ssl
|
||||
|
||||
_geoip_cache = {}
|
||||
|
||||
# GeoIP database reader (local MaxMind database)
|
||||
_geoip_reader = None
|
||||
_geoip_cache = {}
|
||||
GEOIP_DB_PATH = "/var/lib/secubox/geoip/GeoLite2-Country.mmdb"
|
||||
|
||||
def _get_geoip_reader():
|
||||
global _geoip_reader
|
||||
if _geoip_reader is None and geoip2:
|
||||
try:
|
||||
_geoip_reader = geoip2.database.Reader(GEOIP_DB_PATH)
|
||||
except:
|
||||
pass
|
||||
return _geoip_reader
|
||||
|
||||
|
||||
@app.get("/geoip/{ip}")
|
||||
async def get_geoip(ip: str):
|
||||
"""Lookup country code for IP address using local MaxMind database."""
|
||||
if ip in _geoip_cache:
|
||||
return {"ip": ip, "country": _geoip_cache[ip]}
|
||||
|
||||
# Skip private IPs
|
||||
if ip.startswith(("10.", "192.168.", "127.", "172.16.", "172.17.", "172.18.", "172.19.")):
|
||||
_geoip_cache[ip] = "LAN"
|
||||
return {"ip": ip, "country": "LAN"}
|
||||
|
||||
# Try local database
|
||||
reader = _get_geoip_reader()
|
||||
if reader:
|
||||
try:
|
||||
response = reader.country(ip)
|
||||
country = response.country.iso_code or ""
|
||||
if country:
|
||||
_geoip_cache[ip] = country
|
||||
return {"ip": ip, "country": country}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {"ip": ip, "country": ""}
|
||||
|
|
|
|||
|
|
@ -414,6 +414,36 @@
|
|||
return res.json();
|
||||
} catch { return {}; }
|
||||
}
|
||||
// GeoIP cache for country flags
|
||||
const GEOIP_CACHE_KEY = "secubox_waf_geoip";
|
||||
const GEOIP_CACHE_TTL = 86400000;
|
||||
function getGeoCache() { try { return JSON.parse(localStorage.getItem(GEOIP_CACHE_KEY) || "{}"); } catch { return {}; } }
|
||||
function setGeoCache(ip, country) { const c = getGeoCache(); c[ip] = { country, ts: Date.now() }; localStorage.setItem(GEOIP_CACHE_KEY, JSON.stringify(c)); }
|
||||
function countryToFlag(code) { if (!code || code === "LAN" || code.length !== 2) return ""; return String.fromCodePoint(...[...code.toUpperCase()].map(c => 127397 + c.charCodeAt(0))); }
|
||||
function getSiteEmoji(host) {
|
||||
if (!host) return "🌐";
|
||||
const h = host.toLowerCase();
|
||||
if (h.includes("git")) return "💻";
|
||||
if (h.includes("mail")) return "📧";
|
||||
if (h.includes("blog")) return "📝";
|
||||
if (h.includes("admin")) return "🔐";
|
||||
if (h.includes("api")) return "⚡";
|
||||
if (h.includes("auth")) return "🔑";
|
||||
if (h.includes("file") || h.includes("cloud")) return "📁";
|
||||
if (h.includes("chat") || h.includes("matrix")) return "💬";
|
||||
if (h.includes("media") || h.includes("stream")) return "🎬";
|
||||
if (h.includes("secubox")) return "🛡️";
|
||||
return "🌐";
|
||||
}
|
||||
|
||||
async function lookupGeoIP(ip) { if (!ip) return "";
|
||||
const cache = getGeoCache();
|
||||
if (cache[ip] && Date.now() - cache[ip].ts < GEOIP_CACHE_TTL) return cache[ip].country;
|
||||
if (ip.startsWith("10.") || ip.startsWith("192.168.") || ip.startsWith("127.")) { setGeoCache(ip, "LAN"); return "LAN"; }
|
||||
try { const r = await fetch("/api/v1/waf/geoip/" + ip, { mode: "cors" }); const j = await r.json(); const c = j.country; if (c && c.length === 2) { setGeoCache(ip, c); return c; } } catch {}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
function switchTab(tabName) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
|
|
@ -464,16 +494,21 @@
|
|||
}
|
||||
|
||||
async function loadAlerts() {
|
||||
const d = await api('/alerts?limit=30');
|
||||
const d = await api('/alerts?limit=10');
|
||||
const el = document.getElementById('alertsList');
|
||||
if (!d.alerts || d.alerts.length === 0) {
|
||||
el.innerHTML = '<p style="color:var(--text-dim)">No recent alerts</p>';
|
||||
return;
|
||||
}
|
||||
// Fetch flags
|
||||
const ips = [...new Set(d.alerts.map(a => a.client_ip))];
|
||||
const flags = {};
|
||||
await Promise.all(ips.map(async ip => { flags[ip] = countryToFlag(await lookupGeoIP(ip)); }));
|
||||
el.innerHTML = d.alerts.map(a => `
|
||||
<div class="alert-row">
|
||||
<span class="alert-time">${new Date(a.timestamp).toLocaleString()}</span>
|
||||
<span class="alert-ip">${a.ip}</span>
|
||||
<span class="alert-flag" style="font-size:1.2em">${flags[a.client_ip] || ""}</span>
|
||||
<span class="alert-ip">${a.client_ip}</span><span title="${a.host}" style="margin-left:4px">${getSiteEmoji(a.host)}</span>
|
||||
<span class="badge ${a.severity}">${a.severity}</span>
|
||||
<span class="alert-rule">${a.rule_id}: ${a.description}</span>
|
||||
</div>
|
||||
|
|
@ -514,14 +549,20 @@
|
|||
tbody.innerHTML = '<tr><td colspan="6" style="color:var(--text-dim)">No active bans</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = d.bans.map(b => `
|
||||
// Fetch flags for bans
|
||||
const banIps = [...new Set(d.bans.map(b => b.value || b.ip).filter(Boolean))];
|
||||
const banFlags = {};
|
||||
await Promise.all(banIps.map(async ip => { banFlags[ip] = countryToFlag(await lookupGeoIP(ip)); }));
|
||||
tbody.innerHTML = d.bans.map(b => {
|
||||
const ip = b.value || b.ip || '-';
|
||||
return `
|
||||
<tr>
|
||||
<td style="font-family:monospace">${b.value || b.ip || '-'}</td>
|
||||
<td><span style="font-size:1.2em">${banFlags[ip] || ''}</span> <span style="font-family:monospace">${ip}</span></td>
|
||||
<td>${b.scenario || b.reason || '-'}</td>
|
||||
<td>${b.duration || '-'}</td>
|
||||
<td><button class="btn" onclick="unbanIp('${b.value || b.ip}')">Unban</button></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
async function toggleCategory(cat, enabled) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user