feat: WinDHCPD — Windows-native DHCP server
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
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
"""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])
|
||||
Reference in New Issue
Block a user