Files
win-dhcpd/dhcpd/web/app.py
T
Your Name ea8eb2b5f9 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
2026-07-13 16:22:19 +08:00

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])