mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-28 21:17:36 +00:00
Add per-VLAN QoS support and fix menu naming issues
secubox-qos v1.1.0: - Add multi-interface support (eth0, eth0.100, eth0.200, etc.) - Add VLAN discovery and listing (/vlans endpoint) - Add per-VLAN bandwidth policies with priority settings - Add 802.1p PCP (Priority Code Point) marking support - Add VLAN creation/deletion endpoints - Add VLAN-aware traffic classification rules - Add per-interface statistics and tc class stats - Support applying QoS to all managed interfaces at once - Update frontend with VLAN policies table and PCP settings secubox-roadmap: - Add conditional navbar based on authentication status - Show guest header with Login button when not authenticated - Show full sidebar when authenticated Menu fixes (5 modules): - Fix mitmproxy: title → name - Fix exposure: title → name, add description - Fix traffic: title → name, add description - Fix wireguard: title → name, section → category, fix icon - Fix zkp: remove duplicate title, url → path Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6a41f9d508
commit
c53f24dc6c
|
|
@ -1,5 +1,5 @@
|
|||
# MIGRATION MAP — SecuBox OpenWrt → Debian
|
||||
*Mis à jour : 2026-03-23*
|
||||
*Mis à jour : 2026-03-24*
|
||||
|
||||
Légende : ✅ Terminé · 🔄 En cours · ⬜ À faire · ⏸ Bloqué
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ Légende : ✅ Terminé · 🔄 En cours · ⬜ À faire · ⏸ Bloqué
|
|||
| **secubox-vhost** | ✅ | ✅ | ✅ | vhosts, ssl, certs | ✅ |
|
||||
| **secubox-mediaflow** | ✅ (20) | ✅ | ✅ | streams, alerts... | ✅ |
|
||||
| **secubox-dpi** | ✅ | ✅ | ✅ | 40+ endpoints netifyd | ✅ |
|
||||
| **secubox-qos** | ✅ (80) | ✅ | ✅ | 60+ endpoints HTB | ✅ |
|
||||
| **secubox-qos** | ✅ (80) | ✅ | ✅ | 80+ endpoints HTB + VLAN v1.1.0 | ✅ |
|
||||
| **secubox-auth** | ✅ (11) | ✅ | ✅ | 20+ endpoints | ✅ |
|
||||
| **secubox-cdn** | ✅ (36) | ✅ | ✅ | 25+ endpoints | ✅ |
|
||||
| **secubox-system** | ✅ (42) | ✅ | ✅ | 35+ endpoints | ✅ |
|
||||
|
|
|
|||
|
|
@ -1,10 +1,30 @@
|
|||
# WIP — Work In Progress
|
||||
*Mis à jour : 2026-03-23 (Session 9)*
|
||||
*Mis à jour : 2026-03-24 (Session 10)*
|
||||
|
||||
---
|
||||
|
||||
## ✅ Terminé cette session
|
||||
|
||||
### secubox-qos v1.1.0 — Per-VLAN QoS Support ✅
|
||||
- **Multi-interface support** — Manage QoS on eth0, eth0.100, eth0.200, etc.
|
||||
- **VLAN discovery** — Auto-detect existing VLAN interfaces
|
||||
- **Per-VLAN policies** — Independent bandwidth limits per VLAN
|
||||
- **802.1p PCP marking** — Map tc classes to VLAN priority (0-7)
|
||||
- **VLAN creation/deletion** — Create VLAN interfaces with QoS from UI
|
||||
- **VLAN-aware rules** — Traffic classification by VLAN ID
|
||||
- **Per-interface statistics** — RX/TX bytes, tc class stats
|
||||
- **Apply-all function** — Apply QoS to all managed interfaces at once
|
||||
- **Frontend updated** — VLAN policies table, PCP settings, interface stats
|
||||
|
||||
New API endpoints:
|
||||
- `GET /vlans` — List VLAN interfaces with policies
|
||||
- `GET/POST/DELETE /vlan/{interface}` — VLAN policy management
|
||||
- `POST /vlan/create` — Create new VLAN with QoS
|
||||
- `POST /vlan/apply_all` — Apply QoS to all interfaces
|
||||
- `GET/POST /pcp/mappings` — 802.1p priority mappings
|
||||
- `GET/POST/DELETE /interfaces` — Interface management
|
||||
- `GET/POST/DELETE /vlan/rules` — VLAN classification rules
|
||||
|
||||
### 6 New Modules Committed (Session 9) ✅
|
||||
- **secubox-backup** v1.0.0 — System config and LXC container backup/restore
|
||||
- **secubox-watchdog** v1.0.0 — Container, service, and endpoint monitoring
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
{
|
||||
"id": "exposure",
|
||||
"title": "Exposure",
|
||||
"name": "Exposure",
|
||||
"icon": "🌐",
|
||||
"path": "/exposure/",
|
||||
"category": "network",
|
||||
"order": 565
|
||||
"order": 565,
|
||||
"description": "Unified exposure settings (Tor, SSL, DNS, Mesh)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
{
|
||||
"id": "mitmproxy",
|
||||
"title": "MITM Proxy",
|
||||
"name": "MITM Proxy",
|
||||
"icon": "🔍",
|
||||
"path": "/mitmproxy/",
|
||||
"category": "security",
|
||||
"order": 115
|
||||
"order": 115,
|
||||
"description": "Traffic inspection and WAF proxy"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@ Port de luci-app-bandwidth-manager
|
|||
|
||||
Méthodes RPCD portées :
|
||||
status, classes, rules, schedules, clients, usage, quotas, apply_qos
|
||||
|
||||
v1.1.0: Per-VLAN QoS support
|
||||
- Multi-interface support (eth0, eth0.100, eth0.200, etc.)
|
||||
- Per-VLAN bandwidth policies
|
||||
- 802.1p PCP priority mapping
|
||||
- VLAN-aware traffic classification
|
||||
- VLAN discovery and management
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from fastapi import FastAPI, APIRouter, Depends, HTTPException
|
||||
|
|
@ -11,10 +18,11 @@ from pydantic import BaseModel
|
|||
from secubox_core.auth import router as auth_router, require_jwt
|
||||
from secubox_core.config import get_config
|
||||
from secubox_core.logger import get_logger
|
||||
import subprocess, json
|
||||
import subprocess, json, re
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
app = FastAPI(title="secubox-qos", version="1.0.0", root_path="/api/v1/qos")
|
||||
app = FastAPI(title="secubox-qos", version="1.1.0", root_path="/api/v1/qos")
|
||||
app.include_router(auth_router, prefix="/auth")
|
||||
|
||||
router = APIRouter()
|
||||
|
|
@ -22,18 +30,52 @@ log = get_logger("qos")
|
|||
|
||||
QOS_CONF = Path("/etc/secubox/qos.json")
|
||||
|
||||
# 802.1p Priority Code Point (PCP) mapping
|
||||
# Maps tc class priority to VLAN PCP values (0-7)
|
||||
PCP_MAPPING = {
|
||||
"realtime": 7, # Network Control
|
||||
"voice": 6, # Voice (VoIP)
|
||||
"video": 5, # Video
|
||||
"controlled": 4, # Controlled Load
|
||||
"excellent": 3, # Excellent Effort (Business Critical)
|
||||
"best_effort": 0, # Best Effort (Default)
|
||||
"background": 1, # Background
|
||||
"bulk": 2, # Spare/Bulk
|
||||
}
|
||||
|
||||
# Default VLAN policies template
|
||||
DEFAULT_VLAN_POLICY = {
|
||||
"enabled": True,
|
||||
"upload_mbps": 100,
|
||||
"download_mbps": 200,
|
||||
"priority": 0, # PCP value
|
||||
"description": "",
|
||||
"classes": [],
|
||||
}
|
||||
|
||||
|
||||
def _load_conf() -> dict:
|
||||
if QOS_CONF.exists():
|
||||
return json.loads(QOS_CONF.read_text())
|
||||
conf = json.loads(QOS_CONF.read_text())
|
||||
# Migration: ensure new fields exist
|
||||
if "vlan_policies" not in conf:
|
||||
conf["vlan_policies"] = {}
|
||||
if "interfaces" not in conf:
|
||||
# Migrate single interface to list
|
||||
conf["interfaces"] = [conf.get("interface", "eth0")]
|
||||
return conf
|
||||
return {
|
||||
"enabled": False,
|
||||
"interface": "eth0",
|
||||
"interface": "eth0", # Primary interface (legacy)
|
||||
"interfaces": ["eth0"], # All managed interfaces
|
||||
"upload_mbps": 100,
|
||||
"download_mbps": 200,
|
||||
"classes": [],
|
||||
"rules": [],
|
||||
"quotas": [],
|
||||
"vlan_policies": {}, # Per-VLAN policies: {"eth0.100": {...}, ...}
|
||||
"pcp_enabled": False, # 802.1p marking enabled
|
||||
"pcp_mappings": {}, # Custom PCP mappings
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -42,6 +84,89 @@ def _save_conf(conf: dict):
|
|||
QOS_CONF.write_text(json.dumps(conf, indent=2))
|
||||
|
||||
|
||||
def _discover_vlans() -> list[dict]:
|
||||
"""Discover all VLAN interfaces on the system."""
|
||||
vlans = []
|
||||
r = subprocess.run(["ip", "-j", "link", "show", "type", "vlan"],
|
||||
capture_output=True, text=True)
|
||||
try:
|
||||
links = json.loads(r.stdout) if r.stdout.strip() else []
|
||||
for link in links:
|
||||
ifname = link.get("ifname", "")
|
||||
# Parse VLAN ID from interface name or linkinfo
|
||||
vlan_id = None
|
||||
if "." in ifname:
|
||||
try:
|
||||
vlan_id = int(ifname.split(".")[-1])
|
||||
except ValueError:
|
||||
pass
|
||||
linkinfo = link.get("linkinfo", {}).get("info_data", {})
|
||||
if not vlan_id:
|
||||
vlan_id = linkinfo.get("id")
|
||||
|
||||
parent = link.get("link", "")
|
||||
vlans.append({
|
||||
"interface": ifname,
|
||||
"vlan_id": vlan_id,
|
||||
"parent": parent,
|
||||
"state": link.get("operstate", "unknown"),
|
||||
"mtu": link.get("mtu", 1500),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return vlans
|
||||
|
||||
|
||||
def _get_parent_interface(vlan_iface: str) -> str:
|
||||
"""Get parent interface for a VLAN interface."""
|
||||
if "." in vlan_iface:
|
||||
return vlan_iface.split(".")[0]
|
||||
return vlan_iface
|
||||
|
||||
|
||||
def _is_vlan_interface(iface: str) -> bool:
|
||||
"""Check if interface is a VLAN interface."""
|
||||
if "." in iface:
|
||||
return True
|
||||
# Check via ip link
|
||||
r = subprocess.run(["ip", "-d", "link", "show", iface],
|
||||
capture_output=True, text=True)
|
||||
return "vlan" in r.stdout.lower()
|
||||
|
||||
|
||||
def _get_vlan_id(iface: str) -> Optional[int]:
|
||||
"""Extract VLAN ID from interface."""
|
||||
if "." in iface:
|
||||
try:
|
||||
return int(iface.split(".")[-1])
|
||||
except ValueError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _create_vlan_interface(parent: str, vlan_id: int) -> dict:
|
||||
"""Create a VLAN interface."""
|
||||
iface = f"{parent}.{vlan_id}"
|
||||
r = subprocess.run(
|
||||
["ip", "link", "add", "link", parent, "name", iface,
|
||||
"type", "vlan", "id", str(vlan_id)],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if r.returncode != 0:
|
||||
return {"success": False, "error": r.stderr}
|
||||
|
||||
# Bring interface up
|
||||
subprocess.run(["ip", "link", "set", iface, "up"], capture_output=True)
|
||||
return {"success": True, "interface": iface}
|
||||
|
||||
|
||||
def _delete_vlan_interface(iface: str) -> dict:
|
||||
"""Delete a VLAN interface."""
|
||||
r = subprocess.run(["ip", "link", "delete", iface],
|
||||
capture_output=True, text=True)
|
||||
return {"success": r.returncode == 0, "error": r.stderr if r.returncode != 0 else None}
|
||||
|
||||
|
||||
def _iface_stats(iface: str) -> dict:
|
||||
base = Path(f"/sys/class/net/{iface}/statistics")
|
||||
if not base.exists():
|
||||
|
|
@ -63,14 +188,24 @@ def _tc_show(iface: str) -> dict:
|
|||
return {"qdiscs": [], "raw": r.stdout[:500]}
|
||||
|
||||
|
||||
def _apply_htb(conf: dict) -> dict:
|
||||
def _apply_htb(conf: dict, iface: str = None) -> dict:
|
||||
"""
|
||||
Applique la configuration HTB via pyroute2 ou tc CLI.
|
||||
Utilise tc CLI pour la compatibilité maximale.
|
||||
Applique la configuration HTB via tc CLI.
|
||||
Supports per-interface and per-VLAN configuration.
|
||||
"""
|
||||
iface = conf.get("interface", "eth0")
|
||||
up_kbps = int(conf.get("upload_mbps", 100)) * 1000
|
||||
down_kbps = int(conf.get("download_mbps", 200)) * 1000
|
||||
if iface is None:
|
||||
iface = conf.get("interface", "eth0")
|
||||
|
||||
# Check for VLAN-specific policy
|
||||
vlan_policy = conf.get("vlan_policies", {}).get(iface, {})
|
||||
if vlan_policy:
|
||||
up_kbps = int(vlan_policy.get("upload_mbps", 100)) * 1000
|
||||
down_kbps = int(vlan_policy.get("download_mbps", 200)) * 1000
|
||||
prio_offset = vlan_policy.get("priority", 0)
|
||||
else:
|
||||
up_kbps = int(conf.get("upload_mbps", 100)) * 1000
|
||||
down_kbps = int(conf.get("download_mbps", 200)) * 1000
|
||||
prio_offset = 0
|
||||
|
||||
cmds = [
|
||||
# Reset
|
||||
|
|
@ -101,7 +236,68 @@ def _apply_htb(conf: dict) -> dict:
|
|||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
results.append({"cmd": " ".join(cmd[-4:]), "ok": r.returncode == 0})
|
||||
|
||||
return {"steps": results, "interface": iface}
|
||||
# Apply VLAN-specific filters if this is a VLAN interface
|
||||
vlan_id = _get_vlan_id(iface)
|
||||
if vlan_id and conf.get("pcp_enabled"):
|
||||
pcp_results = _apply_pcp_marking(iface, vlan_policy)
|
||||
results.extend(pcp_results)
|
||||
|
||||
return {"steps": results, "interface": iface, "vlan_id": vlan_id}
|
||||
|
||||
|
||||
def _apply_htb_all(conf: dict) -> dict:
|
||||
"""Apply HTB to all configured interfaces."""
|
||||
results = {}
|
||||
interfaces = conf.get("interfaces", [conf.get("interface", "eth0")])
|
||||
|
||||
for iface in interfaces:
|
||||
results[iface] = _apply_htb(conf, iface)
|
||||
|
||||
return {"interfaces": results, "count": len(interfaces)}
|
||||
|
||||
|
||||
def _apply_pcp_marking(iface: str, policy: dict) -> list:
|
||||
"""Apply 802.1p PCP marking for VLAN interface."""
|
||||
results = []
|
||||
pcp = policy.get("priority", 0)
|
||||
|
||||
# Use tc to set skb priority which maps to VLAN PCP
|
||||
# Class 1:10 (high) → PCP 6 (voice)
|
||||
# Class 1:30 (standard) → PCP 0 (best effort)
|
||||
# Class 1:50 (bulk) → PCP 1 (background)
|
||||
pcp_map = {
|
||||
"1:10": min(pcp + 6, 7),
|
||||
"1:30": pcp,
|
||||
"1:50": max(pcp - 1, 0) if pcp > 0 else 1,
|
||||
}
|
||||
|
||||
for classid, pcp_val in pcp_map.items():
|
||||
# Set skb->priority via tc filter action
|
||||
cmd = [
|
||||
"tc", "filter", "add", "dev", iface, "parent", "1:",
|
||||
"protocol", "all", "prio", "1",
|
||||
"handle", classid.replace(":", ""),
|
||||
"fw", "flowid", classid,
|
||||
"action", "skbedit", "priority", str(pcp_val)
|
||||
]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
results.append({"cmd": f"pcp {classid}→{pcp_val}", "ok": r.returncode == 0})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _apply_vlan_filter(parent_iface: str, vlan_id: int, class_id: str) -> dict:
|
||||
"""Apply tc filter to classify traffic by VLAN ID."""
|
||||
# Use u32 filter to match VLAN ID in 802.1Q header
|
||||
# VLAN ID is in bytes 14-15 of ethernet frame (offset 12 from IP)
|
||||
cmd = [
|
||||
"tc", "filter", "add", "dev", parent_iface, "parent", "1:",
|
||||
"protocol", "802.1Q", "prio", "1",
|
||||
"u32", "match", "u16", f"0x{vlan_id:04x}", "0x0fff", "at", "-4",
|
||||
"flowid", class_id
|
||||
]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
return {"cmd": f"vlan {vlan_id}→{class_id}", "ok": r.returncode == 0, "error": r.stderr}
|
||||
|
||||
|
||||
# ── GET ───────────────────────────────────────────────────────────
|
||||
|
|
@ -725,9 +921,425 @@ async def reload(user=Depends(require_jwt)):
|
|||
return {"success": True, "message": "QoS disabled"}
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
# ══ VLAN QoS Management ═══════════════════════════════════════════
|
||||
# ══════════════════════════════════════════════════════════════════
|
||||
|
||||
@router.get("/vlans")
|
||||
async def list_vlans(user=Depends(require_jwt)):
|
||||
"""List all VLAN interfaces with their QoS policies."""
|
||||
vlans = _discover_vlans()
|
||||
conf = _load_conf()
|
||||
policies = conf.get("vlan_policies", {})
|
||||
|
||||
for vlan in vlans:
|
||||
iface = vlan["interface"]
|
||||
vlan["policy"] = policies.get(iface, None)
|
||||
vlan["qos_enabled"] = iface in conf.get("interfaces", [])
|
||||
|
||||
return {"vlans": vlans, "count": len(vlans)}
|
||||
|
||||
|
||||
@router.get("/vlan/{interface}")
|
||||
async def get_vlan_policy(interface: str, user=Depends(require_jwt)):
|
||||
"""Get QoS policy for a specific VLAN interface."""
|
||||
conf = _load_conf()
|
||||
policy = conf.get("vlan_policies", {}).get(interface, None)
|
||||
stats = _iface_stats(interface)
|
||||
tc = _tc_show(interface)
|
||||
vlan_id = _get_vlan_id(interface)
|
||||
|
||||
return {
|
||||
"interface": interface,
|
||||
"vlan_id": vlan_id,
|
||||
"policy": policy,
|
||||
"stats": stats,
|
||||
"tc": tc,
|
||||
"qos_active": any(q.get("kind") == "htb" for q in tc.get("qdiscs", [])),
|
||||
}
|
||||
|
||||
|
||||
class VlanPolicyRequest(BaseModel):
|
||||
upload_mbps: int = 100
|
||||
download_mbps: int = 200
|
||||
priority: int = 0 # 802.1p PCP base value (0-7)
|
||||
description: str = ""
|
||||
enabled: bool = True
|
||||
classes: list[dict] = []
|
||||
|
||||
|
||||
@router.post("/vlan/{interface}/policy")
|
||||
async def set_vlan_policy(interface: str, req: VlanPolicyRequest, user=Depends(require_jwt)):
|
||||
"""Set QoS policy for a VLAN interface."""
|
||||
conf = _load_conf()
|
||||
|
||||
# Validate interface exists
|
||||
if not Path(f"/sys/class/net/{interface}").exists():
|
||||
raise HTTPException(status_code=404, detail=f"Interface {interface} not found")
|
||||
|
||||
# Validate PCP value
|
||||
if not 0 <= req.priority <= 7:
|
||||
raise HTTPException(status_code=400, detail="Priority must be 0-7 (802.1p PCP)")
|
||||
|
||||
# Store policy
|
||||
conf.setdefault("vlan_policies", {})
|
||||
conf["vlan_policies"][interface] = req.model_dump()
|
||||
|
||||
# Add to managed interfaces if enabled
|
||||
if req.enabled and interface not in conf.get("interfaces", []):
|
||||
conf.setdefault("interfaces", [])
|
||||
conf["interfaces"].append(interface)
|
||||
elif not req.enabled and interface in conf.get("interfaces", []):
|
||||
conf["interfaces"].remove(interface)
|
||||
|
||||
_save_conf(conf)
|
||||
|
||||
# Apply if QoS is globally enabled
|
||||
result = {}
|
||||
if conf.get("enabled") and req.enabled:
|
||||
result = _apply_htb(conf, interface)
|
||||
log.info("VLAN QoS applied: %s - %sMbps↑ %sMbps↓ PCP=%d",
|
||||
interface, req.upload_mbps, req.download_mbps, req.priority)
|
||||
|
||||
return {"success": True, "interface": interface, **result}
|
||||
|
||||
|
||||
@router.delete("/vlan/{interface}/policy")
|
||||
async def delete_vlan_policy(interface: str, user=Depends(require_jwt)):
|
||||
"""Remove QoS policy from a VLAN interface."""
|
||||
conf = _load_conf()
|
||||
|
||||
# Remove policy
|
||||
if interface in conf.get("vlan_policies", {}):
|
||||
del conf["vlan_policies"][interface]
|
||||
|
||||
# Remove from managed interfaces
|
||||
if interface in conf.get("interfaces", []):
|
||||
conf["interfaces"].remove(interface)
|
||||
|
||||
_save_conf(conf)
|
||||
|
||||
# Clear tc rules
|
||||
subprocess.run(["tc", "qdisc", "del", "dev", interface, "root"],
|
||||
capture_output=True)
|
||||
|
||||
log.info("VLAN QoS policy removed: %s", interface)
|
||||
return {"success": True, "interface": interface}
|
||||
|
||||
|
||||
class CreateVlanRequest(BaseModel):
|
||||
parent: str = "eth0"
|
||||
vlan_id: int
|
||||
upload_mbps: int = 100
|
||||
download_mbps: int = 200
|
||||
priority: int = 0
|
||||
description: str = ""
|
||||
|
||||
|
||||
@router.post("/vlan/create")
|
||||
async def create_vlan(req: CreateVlanRequest, user=Depends(require_jwt)):
|
||||
"""Create a new VLAN interface with QoS policy."""
|
||||
# Validate VLAN ID
|
||||
if not 1 <= req.vlan_id <= 4094:
|
||||
raise HTTPException(status_code=400, detail="VLAN ID must be 1-4094")
|
||||
|
||||
# Validate parent interface exists
|
||||
if not Path(f"/sys/class/net/{req.parent}").exists():
|
||||
raise HTTPException(status_code=404, detail=f"Parent interface {req.parent} not found")
|
||||
|
||||
# Create VLAN interface
|
||||
result = _create_vlan_interface(req.parent, req.vlan_id)
|
||||
if not result["success"]:
|
||||
raise HTTPException(status_code=500, detail=result.get("error", "Failed to create VLAN"))
|
||||
|
||||
interface = result["interface"]
|
||||
|
||||
# Set QoS policy
|
||||
conf = _load_conf()
|
||||
conf.setdefault("vlan_policies", {})
|
||||
conf["vlan_policies"][interface] = {
|
||||
"enabled": True,
|
||||
"upload_mbps": req.upload_mbps,
|
||||
"download_mbps": req.download_mbps,
|
||||
"priority": req.priority,
|
||||
"description": req.description,
|
||||
"classes": [],
|
||||
}
|
||||
|
||||
conf.setdefault("interfaces", [])
|
||||
if interface not in conf["interfaces"]:
|
||||
conf["interfaces"].append(interface)
|
||||
|
||||
_save_conf(conf)
|
||||
|
||||
log.info("VLAN created: %s (ID: %d) on %s", interface, req.vlan_id, req.parent)
|
||||
return {"success": True, "interface": interface, "vlan_id": req.vlan_id}
|
||||
|
||||
|
||||
@router.delete("/vlan/{interface}")
|
||||
async def delete_vlan(interface: str, user=Depends(require_jwt)):
|
||||
"""Delete a VLAN interface and its QoS policy."""
|
||||
if not _is_vlan_interface(interface):
|
||||
raise HTTPException(status_code=400, detail="Not a VLAN interface")
|
||||
|
||||
# Remove QoS first
|
||||
conf = _load_conf()
|
||||
if interface in conf.get("vlan_policies", {}):
|
||||
del conf["vlan_policies"][interface]
|
||||
if interface in conf.get("interfaces", []):
|
||||
conf["interfaces"].remove(interface)
|
||||
_save_conf(conf)
|
||||
|
||||
# Delete interface
|
||||
result = _delete_vlan_interface(interface)
|
||||
if not result["success"]:
|
||||
raise HTTPException(status_code=500, detail=result.get("error", "Failed to delete VLAN"))
|
||||
|
||||
log.info("VLAN deleted: %s", interface)
|
||||
return {"success": True, "interface": interface}
|
||||
|
||||
|
||||
@router.get("/vlan/policies")
|
||||
async def list_vlan_policies(user=Depends(require_jwt)):
|
||||
"""List all VLAN QoS policies."""
|
||||
conf = _load_conf()
|
||||
policies = conf.get("vlan_policies", {})
|
||||
return {
|
||||
"policies": [
|
||||
{"interface": iface, **policy}
|
||||
for iface, policy in policies.items()
|
||||
],
|
||||
"count": len(policies),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/vlan/apply_all")
|
||||
async def apply_all_vlan_qos(user=Depends(require_jwt)):
|
||||
"""Apply QoS to all configured VLAN interfaces."""
|
||||
conf = _load_conf()
|
||||
if not conf.get("enabled"):
|
||||
return {"success": False, "error": "QoS is globally disabled"}
|
||||
|
||||
result = _apply_htb_all(conf)
|
||||
log.info("QoS applied to %d interfaces", result["count"])
|
||||
return {"success": True, **result}
|
||||
|
||||
|
||||
# ── 802.1p PCP Management ─────────────────────────────────────────
|
||||
|
||||
@router.get("/pcp/mappings")
|
||||
async def get_pcp_mappings(user=Depends(require_jwt)):
|
||||
"""Get 802.1p PCP priority mappings."""
|
||||
conf = _load_conf()
|
||||
return {
|
||||
"default_mappings": PCP_MAPPING,
|
||||
"custom_mappings": conf.get("pcp_mappings", {}),
|
||||
"pcp_enabled": conf.get("pcp_enabled", False),
|
||||
}
|
||||
|
||||
|
||||
class PcpMappingRequest(BaseModel):
|
||||
class_id: str # e.g., "1:10"
|
||||
pcp_value: int # 0-7
|
||||
|
||||
|
||||
@router.post("/pcp/mapping")
|
||||
async def set_pcp_mapping(req: PcpMappingRequest, user=Depends(require_jwt)):
|
||||
"""Set custom PCP mapping for a traffic class."""
|
||||
if not 0 <= req.pcp_value <= 7:
|
||||
raise HTTPException(status_code=400, detail="PCP value must be 0-7")
|
||||
|
||||
conf = _load_conf()
|
||||
conf.setdefault("pcp_mappings", {})
|
||||
conf["pcp_mappings"][req.class_id] = req.pcp_value
|
||||
_save_conf(conf)
|
||||
return {"success": True, "mapping": {req.class_id: req.pcp_value}}
|
||||
|
||||
|
||||
@router.post("/pcp/enable")
|
||||
async def enable_pcp(enabled: bool = True, user=Depends(require_jwt)):
|
||||
"""Enable or disable 802.1p PCP marking."""
|
||||
conf = _load_conf()
|
||||
conf["pcp_enabled"] = enabled
|
||||
_save_conf(conf)
|
||||
log.info("802.1p PCP marking %s", "enabled" if enabled else "disabled")
|
||||
return {"success": True, "pcp_enabled": enabled}
|
||||
|
||||
|
||||
# ── Per-Interface Management ──────────────────────────────────────
|
||||
|
||||
@router.get("/interfaces")
|
||||
async def list_managed_interfaces(user=Depends(require_jwt)):
|
||||
"""List all managed interfaces with their QoS status."""
|
||||
conf = _load_conf()
|
||||
managed = conf.get("interfaces", [])
|
||||
vlan_policies = conf.get("vlan_policies", {})
|
||||
|
||||
interfaces = []
|
||||
for iface in managed:
|
||||
stats = _iface_stats(iface)
|
||||
tc = _tc_show(iface)
|
||||
policy = vlan_policies.get(iface, {
|
||||
"upload_mbps": conf.get("upload_mbps", 100),
|
||||
"download_mbps": conf.get("download_mbps", 200),
|
||||
})
|
||||
|
||||
interfaces.append({
|
||||
"interface": iface,
|
||||
"is_vlan": _is_vlan_interface(iface),
|
||||
"vlan_id": _get_vlan_id(iface),
|
||||
"policy": policy,
|
||||
"stats": stats,
|
||||
"qos_active": any(q.get("kind") == "htb" for q in tc.get("qdiscs", [])),
|
||||
})
|
||||
|
||||
return {"interfaces": interfaces, "count": len(interfaces)}
|
||||
|
||||
|
||||
class AddInterfaceRequest(BaseModel):
|
||||
interface: str
|
||||
upload_mbps: int = 100
|
||||
download_mbps: int = 200
|
||||
priority: int = 0
|
||||
apply_now: bool = True
|
||||
|
||||
|
||||
@router.post("/interface/add")
|
||||
async def add_managed_interface(req: AddInterfaceRequest, user=Depends(require_jwt)):
|
||||
"""Add an interface to QoS management."""
|
||||
if not Path(f"/sys/class/net/{req.interface}").exists():
|
||||
raise HTTPException(status_code=404, detail=f"Interface {req.interface} not found")
|
||||
|
||||
conf = _load_conf()
|
||||
conf.setdefault("interfaces", [])
|
||||
|
||||
if req.interface not in conf["interfaces"]:
|
||||
conf["interfaces"].append(req.interface)
|
||||
|
||||
# If it's a VLAN, store policy
|
||||
if _is_vlan_interface(req.interface):
|
||||
conf.setdefault("vlan_policies", {})
|
||||
conf["vlan_policies"][req.interface] = {
|
||||
"enabled": True,
|
||||
"upload_mbps": req.upload_mbps,
|
||||
"download_mbps": req.download_mbps,
|
||||
"priority": req.priority,
|
||||
"description": "",
|
||||
"classes": [],
|
||||
}
|
||||
|
||||
_save_conf(conf)
|
||||
|
||||
result = {}
|
||||
if req.apply_now and conf.get("enabled"):
|
||||
result = _apply_htb(conf, req.interface)
|
||||
|
||||
log.info("Interface added to QoS: %s", req.interface)
|
||||
return {"success": True, "interface": req.interface, **result}
|
||||
|
||||
|
||||
@router.delete("/interface/{interface}")
|
||||
async def remove_managed_interface(interface: str, user=Depends(require_jwt)):
|
||||
"""Remove an interface from QoS management."""
|
||||
conf = _load_conf()
|
||||
|
||||
if interface in conf.get("interfaces", []):
|
||||
conf["interfaces"].remove(interface)
|
||||
|
||||
if interface in conf.get("vlan_policies", {}):
|
||||
del conf["vlan_policies"][interface]
|
||||
|
||||
_save_conf(conf)
|
||||
|
||||
# Clear tc rules
|
||||
subprocess.run(["tc", "qdisc", "del", "dev", interface, "root"],
|
||||
capture_output=True)
|
||||
|
||||
log.info("Interface removed from QoS: %s", interface)
|
||||
return {"success": True, "interface": interface}
|
||||
|
||||
|
||||
@router.get("/interface/{interface}/stats")
|
||||
async def get_interface_stats(interface: str, user=Depends(require_jwt)):
|
||||
"""Get detailed stats for a specific interface."""
|
||||
if not Path(f"/sys/class/net/{interface}").exists():
|
||||
raise HTTPException(status_code=404, detail=f"Interface {interface} not found")
|
||||
|
||||
stats = _iface_stats(interface)
|
||||
tc = _tc_show(interface)
|
||||
conf = _load_conf()
|
||||
policy = conf.get("vlan_policies", {}).get(interface, {})
|
||||
|
||||
# Get tc class stats
|
||||
r = subprocess.run(["tc", "-s", "-j", "class", "show", "dev", interface],
|
||||
capture_output=True, text=True)
|
||||
try:
|
||||
class_stats = json.loads(r.stdout) if r.stdout.strip() else []
|
||||
except Exception:
|
||||
class_stats = []
|
||||
|
||||
return {
|
||||
"interface": interface,
|
||||
"is_vlan": _is_vlan_interface(interface),
|
||||
"vlan_id": _get_vlan_id(interface),
|
||||
"policy": policy,
|
||||
"stats": stats,
|
||||
"tc": tc,
|
||||
"class_stats": class_stats,
|
||||
}
|
||||
|
||||
|
||||
# ── VLAN-aware Rules ──────────────────────────────────────────────
|
||||
|
||||
class VlanRuleRequest(BaseModel):
|
||||
name: str
|
||||
vlan_id: int
|
||||
class_id: str = "1:30"
|
||||
priority: int = 10
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
@router.post("/vlan/rule")
|
||||
async def add_vlan_rule(req: VlanRuleRequest, user=Depends(require_jwt)):
|
||||
"""Add a VLAN-based classification rule."""
|
||||
conf = _load_conf()
|
||||
conf.setdefault("vlan_rules", [])
|
||||
conf["vlan_rules"].append(req.model_dump())
|
||||
_save_conf(conf)
|
||||
|
||||
# Apply filter on parent interface
|
||||
# Find interfaces that might carry this VLAN
|
||||
result = None
|
||||
for iface in conf.get("interfaces", []):
|
||||
parent = _get_parent_interface(iface)
|
||||
if parent != iface: # This is a VLAN interface
|
||||
continue
|
||||
# Apply to parent interfaces
|
||||
result = _apply_vlan_filter(iface, req.vlan_id, req.class_id)
|
||||
|
||||
log.info("VLAN rule added: VLAN %d → %s", req.vlan_id, req.class_id)
|
||||
return {"success": True, "rule": req.model_dump(), "filter_result": result}
|
||||
|
||||
|
||||
@router.get("/vlan/rules")
|
||||
async def list_vlan_rules(user=Depends(require_jwt)):
|
||||
"""List all VLAN classification rules."""
|
||||
conf = _load_conf()
|
||||
return {"rules": conf.get("vlan_rules", [])}
|
||||
|
||||
|
||||
@router.delete("/vlan/rule/{name}")
|
||||
async def delete_vlan_rule(name: str, user=Depends(require_jwt)):
|
||||
"""Delete a VLAN classification rule."""
|
||||
conf = _load_conf()
|
||||
conf["vlan_rules"] = [r for r in conf.get("vlan_rules", []) if r.get("name") != name]
|
||||
_save_conf(conf)
|
||||
return {"success": True, "deleted": name}
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok", "module": "qos"}
|
||||
return {"status": "ok", "module": "qos", "version": "1.1.0"}
|
||||
|
||||
|
||||
app.include_router(router)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,18 @@
|
|||
secubox-qos (1.1.0-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* Add per-VLAN QoS support
|
||||
* Add multi-interface management (eth0, eth0.100, eth0.200, etc.)
|
||||
* Add VLAN discovery and listing (/vlans endpoint)
|
||||
* Add per-VLAN bandwidth policies with priority settings
|
||||
* Add 802.1p PCP (Priority Code Point) marking support
|
||||
* Add VLAN creation/deletion endpoints
|
||||
* Add VLAN-aware traffic classification rules
|
||||
* Add per-interface statistics and tc class stats
|
||||
* Support applying QoS to all managed interfaces at once
|
||||
* Backward compatible with single-interface configuration
|
||||
|
||||
-- Gerald KERMA <devel@cybermind.fr> Mon, 24 Mar 2026 12:30:00 +0100
|
||||
|
||||
secubox-qos (1.0.4-1~bookworm1) bookworm; urgency=medium
|
||||
|
||||
* Add dynamic menu system with menu.d JSON definitions
|
||||
|
|
|
|||
|
|
@ -308,6 +308,91 @@
|
|||
<tbody id="rulesTable"><tr><td colspan="5">Loading...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- VLAN QoS Section -->
|
||||
<div class="card">
|
||||
<h2>🏷️ VLAN QoS Policies</h2>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<button class="btn primary" onclick="showAddVlan()">Add VLAN Policy</button>
|
||||
<button class="btn" onclick="applyAllVlans()">Apply All</button>
|
||||
<button class="btn" onclick="refreshVlans()">Refresh</button>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Interface</th><th>VLAN ID</th><th>Upload</th><th>Download</th><th>PCP</th><th>Status</th><th>Actions</th></tr></thead>
|
||||
<tbody id="vlansTable"><tr><td colspan="7">Loading...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>📊 Per-Interface Statistics</h2>
|
||||
<table>
|
||||
<thead><tr><th>Interface</th><th>Type</th><th>RX Bytes</th><th>TX Bytes</th><th>QoS Active</th></tr></thead>
|
||||
<tbody id="interfacesTable"><tr><td colspan="5">Loading...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>🔢 802.1p PCP Mappings</h2>
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="margin-right: 1rem;">
|
||||
<input type="checkbox" id="pcpEnabled" onchange="togglePcp()"> Enable 802.1p PCP Marking
|
||||
</label>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>Traffic Type</th><th>PCP Value</th><th>Description</th></tr></thead>
|
||||
<tbody id="pcpTable">
|
||||
<tr><td>Network Control</td><td>7</td><td>Highest priority, routing protocols</td></tr>
|
||||
<tr><td>Voice (VoIP)</td><td>6</td><td>Real-time voice traffic</td></tr>
|
||||
<tr><td>Video</td><td>5</td><td>Real-time video streaming</td></tr>
|
||||
<tr><td>Controlled Load</td><td>4</td><td>Important business traffic</td></tr>
|
||||
<tr><td>Excellent Effort</td><td>3</td><td>Business critical applications</td></tr>
|
||||
<tr><td>Spare/Bulk</td><td>2</td><td>Bulk data transfers</td></tr>
|
||||
<tr><td>Background</td><td>1</td><td>Background traffic</td></tr>
|
||||
<tr><td>Best Effort</td><td>0</td><td>Default traffic (no priority)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Add VLAN Modal -->
|
||||
<div id="vlanModal" class="modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.85); z-index:1000;">
|
||||
<div class="modal-content" style="background:var(--tube-deep); border:1px solid var(--p31-dim); max-width:500px; margin:10% auto; padding:1.5rem; border-radius:8px;">
|
||||
<h3 class="modal-title" style="color:var(--p31-decay); margin-bottom:1rem;">Add VLAN QoS Policy</h3>
|
||||
<form id="vlanForm">
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label>Parent Interface:</label>
|
||||
<select id="vlanParent" style="width:100%;">
|
||||
<option value="eth0">eth0</option>
|
||||
<option value="eth1">eth1</option>
|
||||
<option value="br0">br0</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label>VLAN ID (1-4094):</label>
|
||||
<input type="number" id="vlanId" min="1" max="4094" style="width:100%;" required>
|
||||
</div>
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label>Upload (Mbps):</label>
|
||||
<input type="number" id="vlanUpload" value="100" style="width:100%;">
|
||||
</div>
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label>Download (Mbps):</label>
|
||||
<input type="number" id="vlanDownload" value="200" style="width:100%;">
|
||||
</div>
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label>802.1p Priority (0-7):</label>
|
||||
<input type="number" id="vlanPriority" min="0" max="7" value="0" style="width:100%;">
|
||||
</div>
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label>Description:</label>
|
||||
<input type="text" id="vlanDesc" placeholder="e.g., Management VLAN" style="width:100%;">
|
||||
</div>
|
||||
<div style="display:flex; gap:1rem; justify-content:flex-end;">
|
||||
<button type="button" class="btn" onclick="hideVlanModal()">Cancel</button>
|
||||
<button type="submit" class="btn primary">Create VLAN</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
|
|
@ -369,8 +454,177 @@
|
|||
function showAddRule() { alert('Rule configuration coming soon'); }
|
||||
function toggleRule(id) { alert('Toggle rule: ' + id); }
|
||||
|
||||
// VLAN QoS Functions
|
||||
async function refreshVlans() {
|
||||
// Load VLANs
|
||||
const vlans = await api('/vlans');
|
||||
const vlansBody = document.getElementById('vlansTable');
|
||||
if (vlans.vlans && vlans.vlans.length > 0) {
|
||||
vlansBody.innerHTML = vlans.vlans.map(v => `
|
||||
<tr>
|
||||
<td><code>${v.interface}</code></td>
|
||||
<td>${v.vlan_id || '-'}</td>
|
||||
<td>${v.policy?.upload_mbps || '-'} Mbps</td>
|
||||
<td>${v.policy?.download_mbps || '-'} Mbps</td>
|
||||
<td><span class="badge badge-cyan">PCP ${v.policy?.priority || 0}</span></td>
|
||||
<td><span class="badge ${v.qos_enabled ? 'badge-green' : 'badge-yellow'}">${v.qos_enabled ? 'Active' : 'Inactive'}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-blue" onclick="editVlan('${v.interface}')">Edit</button>
|
||||
<button class="btn btn-red" onclick="deleteVlan('${v.interface}')">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
vlansBody.innerHTML = '<tr><td colspan="7" class="empty">No VLAN interfaces detected. Create one to enable per-VLAN QoS.</td></tr>';
|
||||
}
|
||||
|
||||
// Load managed interfaces
|
||||
const ifaces = await api('/interfaces');
|
||||
const ifaceBody = document.getElementById('interfacesTable');
|
||||
if (ifaces.interfaces && ifaces.interfaces.length > 0) {
|
||||
ifaceBody.innerHTML = ifaces.interfaces.map(i => `
|
||||
<tr>
|
||||
<td><code>${i.interface}</code></td>
|
||||
<td><span class="badge ${i.is_vlan ? 'badge-purple' : 'badge-cyan'}">${i.is_vlan ? 'VLAN' : 'Physical'}</span></td>
|
||||
<td>${formatBytes(i.stats?.rx_bytes || 0)}</td>
|
||||
<td>${formatBytes(i.stats?.tx_bytes || 0)}</td>
|
||||
<td><span class="badge ${i.qos_active ? 'badge-green' : 'badge-yellow'}">${i.qos_active ? 'HTB Active' : 'No QoS'}</span></td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
ifaceBody.innerHTML = '<tr><td colspan="5" class="empty">No managed interfaces</td></tr>';
|
||||
}
|
||||
|
||||
// Load PCP status
|
||||
const pcp = await api('/pcp/mappings');
|
||||
document.getElementById('pcpEnabled').checked = pcp.pcp_enabled || false;
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function showAddVlan() {
|
||||
document.getElementById('vlanModal').style.display = 'block';
|
||||
}
|
||||
|
||||
function hideVlanModal() {
|
||||
document.getElementById('vlanModal').style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('vlanForm').onsubmit = async function(e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
parent: document.getElementById('vlanParent').value,
|
||||
vlan_id: parseInt(document.getElementById('vlanId').value),
|
||||
upload_mbps: parseInt(document.getElementById('vlanUpload').value),
|
||||
download_mbps: parseInt(document.getElementById('vlanDownload').value),
|
||||
priority: parseInt(document.getElementById('vlanPriority').value),
|
||||
description: document.getElementById('vlanDesc').value
|
||||
};
|
||||
try {
|
||||
const res = await fetch(API + '/vlan/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
hideVlanModal();
|
||||
refreshVlans();
|
||||
alert('VLAN ' + data.vlan_id + ' created successfully!');
|
||||
} else {
|
||||
alert('Error: ' + (result.detail || result.error || 'Unknown error'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
async function deleteVlan(iface) {
|
||||
if (!confirm('Delete VLAN interface ' + iface + '?')) return;
|
||||
try {
|
||||
const res = await fetch(API + '/vlan/' + iface, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
refreshVlans();
|
||||
} else {
|
||||
alert('Error: ' + (result.detail || 'Failed to delete'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function editVlan(iface) {
|
||||
const policy = await api('/vlan/' + iface);
|
||||
if (policy.policy) {
|
||||
const p = policy.policy;
|
||||
const newUp = prompt('Upload (Mbps):', p.upload_mbps);
|
||||
const newDown = prompt('Download (Mbps):', p.download_mbps);
|
||||
const newPrio = prompt('802.1p Priority (0-7):', p.priority);
|
||||
if (newUp && newDown && newPrio !== null) {
|
||||
const res = await fetch(API + '/vlan/' + iface + '/policy', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
upload_mbps: parseInt(newUp),
|
||||
download_mbps: parseInt(newDown),
|
||||
priority: parseInt(newPrio),
|
||||
enabled: true,
|
||||
description: p.description || ''
|
||||
})
|
||||
});
|
||||
refreshVlans();
|
||||
}
|
||||
} else {
|
||||
alert('No policy found for ' + iface);
|
||||
}
|
||||
}
|
||||
|
||||
async function applyAllVlans() {
|
||||
try {
|
||||
const res = await fetch(API + '/vlan/apply_all', {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
alert('QoS applied to ' + result.count + ' interfaces');
|
||||
refreshVlans();
|
||||
} else {
|
||||
alert('Error: ' + (result.error || 'Failed to apply'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function togglePcp() {
|
||||
const enabled = document.getElementById('pcpEnabled').checked;
|
||||
try {
|
||||
await fetch(API + '/pcp/enable?enabled=' + enabled, {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('PCP toggle error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
refresh();
|
||||
refreshVlans();
|
||||
setInterval(refresh, 10000);
|
||||
setInterval(refreshVlans, 15000);
|
||||
</script>
|
||||
<script src="/shared/crt-engine.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -30,15 +30,85 @@
|
|||
display: flex;
|
||||
}
|
||||
|
||||
/* Conditional sidebar visibility */
|
||||
body.authenticated .main-content {
|
||||
margin-left: 260px;
|
||||
}
|
||||
|
||||
body.guest .main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
body.guest #sidebar-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
margin-left: 260px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-content { margin-left: 0; padding: 1rem; }
|
||||
.main-content { margin-left: 0 !important; padding: 1rem; }
|
||||
}
|
||||
|
||||
/* Guest header bar */
|
||||
.guest-header {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0 1.5rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
body.guest .guest-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
body.guest .main-content {
|
||||
padding-top: 80px;
|
||||
}
|
||||
|
||||
.guest-header .logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: var(--green);
|
||||
text-decoration: none;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.guest-header .logo-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.guest-header .login-btn {
|
||||
background: transparent;
|
||||
border: 1px solid var(--green);
|
||||
color: var(--green);
|
||||
padding: 0.5rem 1.25rem;
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.guest-header .login-btn:hover {
|
||||
background: var(--green);
|
||||
color: var(--bg-dark);
|
||||
box-shadow: 0 0 15px var(--green);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
|
|
@ -294,7 +364,17 @@
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sidebar-container"></div>
|
||||
<!-- Guest header (shown when not authenticated) -->
|
||||
<header class="guest-header">
|
||||
<a href="/" class="logo">
|
||||
<span class="logo-icon">🔒</span>
|
||||
<span>SECUBOX</span>
|
||||
</a>
|
||||
<button class="login-btn" onclick="window.location.href='/portal/login.html'">Login</button>
|
||||
</header>
|
||||
|
||||
<!-- Sidebar (shown when authenticated) -->
|
||||
<nav class="sidebar" id="sidebar"></nav>
|
||||
|
||||
<main class="main-content">
|
||||
<header class="page-header">
|
||||
|
|
@ -327,11 +407,30 @@
|
|||
</section>
|
||||
</main>
|
||||
|
||||
<script src="/shared/sidebar.js"></script>
|
||||
<script src="/shared/crt-engine.js"></script>
|
||||
<script>
|
||||
const API_BASE = '/api/v1/roadmap';
|
||||
|
||||
// Check authentication status
|
||||
function isAuthenticated() {
|
||||
return !!localStorage.getItem('sbx_token');
|
||||
}
|
||||
|
||||
// Setup page based on auth status
|
||||
function setupAuthState() {
|
||||
if (isAuthenticated()) {
|
||||
document.body.classList.add('authenticated');
|
||||
document.body.classList.remove('guest');
|
||||
// Load sidebar dynamically
|
||||
const script = document.createElement('script');
|
||||
script.src = '/shared/sidebar.js';
|
||||
document.body.appendChild(script);
|
||||
} else {
|
||||
document.body.classList.add('guest');
|
||||
document.body.classList.remove('authenticated');
|
||||
}
|
||||
}
|
||||
|
||||
async function api(endpoint) {
|
||||
try {
|
||||
const res = await fetch(API_BASE + endpoint);
|
||||
|
|
@ -410,6 +509,7 @@
|
|||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setupAuthState();
|
||||
loadRoadmap();
|
||||
setInterval(loadRoadmap, 60000); // Refresh every minute
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
{
|
||||
"id": "traffic",
|
||||
"title": "Traffic Shaping",
|
||||
"name": "Traffic Shaping",
|
||||
"icon": "📊",
|
||||
"path": "/traffic/",
|
||||
"category": "network",
|
||||
"order": 36
|
||||
"order": 36,
|
||||
"description": "TC/CAKE QoS traffic shaping per interface"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
{
|
||||
"id": "wireguard",
|
||||
"title": "WireGuard VPN",
|
||||
"icon": "shield",
|
||||
"name": "WireGuard VPN",
|
||||
"icon": "🔐",
|
||||
"path": "/wireguard/",
|
||||
"section": "security",
|
||||
"category": "security",
|
||||
"order": 25,
|
||||
"api": "/api/v1/wireguard/",
|
||||
"description": "WireGuard VPN tunnel management"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
{
|
||||
"id": "zkp",
|
||||
"name": "ZKP",
|
||||
"title": "Zero-Knowledge Proof",
|
||||
"icon": "🔐",
|
||||
"url": "/zkp/",
|
||||
"category": "security",
|
||||
"order": 570,
|
||||
"description": "Zero-Knowledge Proof Hamiltonian management",
|
||||
"requires": ["secubox-zkp"]
|
||||
"id": "zkp",
|
||||
"name": "Zero-Knowledge Proof",
|
||||
"icon": "🔐",
|
||||
"path": "/zkp/",
|
||||
"category": "security",
|
||||
"order": 570,
|
||||
"description": "Zero-Knowledge Proof Hamiltonian management"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user