ea8eb2b5f9
Python 3 + Scapy DHCP server with: - Full DORA handshake (DISCOVER/OFFER/REQUEST/ACK) - Multi-scope support with DHCP relay - Static MAC-to-IP bindings - Lease persistence (JSON) with auto-prune - NACK for invalid requests - Flask web management panel (dark theme, live refresh) - Admin-privileged scapy sniff on UDP 67
122 lines
3.3 KiB
Python
122 lines
3.3 KiB
Python
"""Flask web management panel for WinDHCPD."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import threading
|
|
from typing import Optional
|
|
|
|
from flask import Flask, jsonify, request, render_template
|
|
|
|
from ..config import Config, StaticBinding
|
|
from ..lease import Lease, LeaseDB
|
|
from ..server import DHCPd
|
|
|
|
log = logging.getLogger("dhcpd.web")
|
|
|
|
app = Flask(__name__)
|
|
|
|
# ── global state (set by run.py) ────────────────────────────────────────
|
|
_server: Optional[DHCPd] = None
|
|
_config: Optional[Config] = None
|
|
_db: Optional[LeaseDB] = None
|
|
|
|
|
|
def init(srv: DHCPd, cfg: Config, db: LeaseDB) -> None:
|
|
global _server, _config, _db
|
|
_server = srv
|
|
_config = cfg
|
|
_db = db
|
|
|
|
|
|
def run(host: str, port: int) -> None:
|
|
log.info("Web panel on http://%s:%d", host, port)
|
|
app.run(host=host, port=port, threaded=True, debug=False)
|
|
|
|
|
|
# ── routes ──────────────────────────────────────────────────────────────
|
|
|
|
def _safe_db() -> LeaseDB:
|
|
return _db # type: ignore
|
|
|
|
|
|
@app.route("/")
|
|
def index():
|
|
cfg = _config # type: ignore
|
|
db = _safe_db()
|
|
srv = _server # type: ignore
|
|
leases = db.list_all() if db else []
|
|
scopes = cfg.scopes if cfg else []
|
|
return render_template("index.html",
|
|
scopes=scopes,
|
|
leases=leases,
|
|
stats=srv.stats if srv else {},
|
|
bindings=cfg.bindings if cfg else [],
|
|
count_active=db.count_active() if db else 0,
|
|
count_total=db.count() if db else 0,
|
|
)
|
|
|
|
|
|
@app.route("/api/leases")
|
|
def api_leases():
|
|
db = _safe_db()
|
|
leases = db.list_all() if db else []
|
|
return jsonify([l.to_json() for l in leases])
|
|
|
|
|
|
@app.route("/api/leases/<mac>/release", methods=["POST"])
|
|
def api_release(mac: str):
|
|
db = _safe_db()
|
|
if db:
|
|
db.release(mac)
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
@app.route("/api/leases/prune", methods=["POST"])
|
|
def api_prune():
|
|
db = _safe_db()
|
|
n = 0
|
|
if db:
|
|
n = db.prune_expired()
|
|
return jsonify({"pruned": n})
|
|
|
|
|
|
@app.route("/api/scopes")
|
|
def api_scopes():
|
|
cfg = _config # type: ignore
|
|
out = []
|
|
for sc in cfg.scopes:
|
|
out.append({
|
|
"name": sc.name,
|
|
"network": sc.network,
|
|
"start_ip": str(sc.start_ip),
|
|
"end_ip": str(sc.end_ip),
|
|
"subnet_mask": sc.subnet_mask,
|
|
"router": sc.router,
|
|
"dns_servers": sc.dns_servers,
|
|
"lease_seconds": sc.lease_seconds,
|
|
"enable": sc.enable,
|
|
})
|
|
return jsonify(out)
|
|
|
|
|
|
@app.route("/api/stats")
|
|
def api_stats():
|
|
srv = _server # type: ignore
|
|
db = _safe_db()
|
|
stats: dict = {}
|
|
if srv:
|
|
stats.update(srv.stats)
|
|
if db:
|
|
stats["leases_total"] = db.count()
|
|
stats["leases_active"] = db.count_active()
|
|
return jsonify(stats)
|
|
|
|
|
|
@app.route("/api/bindings")
|
|
def api_bindings():
|
|
cfg = _config # type: ignore
|
|
return jsonify([{"mac": b.mac_display, "ip": b.ip,
|
|
"scope": b.scope_name, "hostname": b.hostname}
|
|
for b in cfg.bindings])
|