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,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
WinDHCPD - Windows-native DHCP server
|
||||
======================================
|
||||
Python 3 + Scapy based lightweight DHCP server for Windows.
|
||||
|
||||
Usage:
|
||||
python run.py # uses dhcpd.yaml in cwd
|
||||
python run.py config.yaml # specify config file
|
||||
python run.py config.yaml -i # specify interface
|
||||
python run.py --help
|
||||
|
||||
Web panel: http://localhost:8080
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from dhcpd.config import Config
|
||||
from dhcpd.lease import LeaseDB
|
||||
from dhcpd.server import DHCPd
|
||||
from dhcpd.web import app as web_app
|
||||
|
||||
# ── logging setup ────────────────────────────────────────────────────────
|
||||
|
||||
def setup_logging(level: str = "INFO", log_file: str = "dhcpd.log") -> None:
|
||||
"""Configure logging to both console and file."""
|
||||
numeric = getattr(logging, level.upper(), logging.INFO)
|
||||
fmt = logging.Formatter(
|
||||
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
# Console handler
|
||||
ch = logging.StreamHandler(sys.stdout)
|
||||
ch.setFormatter(fmt)
|
||||
ch.setLevel(numeric)
|
||||
|
||||
# File handler
|
||||
try:
|
||||
fh = logging.FileHandler(log_file, encoding="utf-8")
|
||||
fh.setFormatter(fmt)
|
||||
fh.setLevel(numeric)
|
||||
except OSError:
|
||||
fh = None
|
||||
|
||||
root = logging.getLogger()
|
||||
root.setLevel(numeric)
|
||||
root.addHandler(ch)
|
||||
if fh:
|
||||
root.addHandler(fh)
|
||||
|
||||
# Scapy is chatty
|
||||
logging.getLogger("scapy").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
# ── main ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="WinDHCPD - DHCP server for Windows")
|
||||
parser.add_argument("config", nargs="?", default="dhcpd.yaml",
|
||||
help="Path to YAML config file (default: dhcpd.yaml)")
|
||||
parser.add_argument("-i", "--interface", default=None,
|
||||
help="Network interface name to listen on")
|
||||
parser.add_argument("--no-web", action="store_true",
|
||||
help="Disable web management panel")
|
||||
parser.add_argument("--web-port", type=int, default=0,
|
||||
help="Web panel port (overrides config)")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load config
|
||||
config_path = args.config
|
||||
if not os.path.exists(config_path):
|
||||
print(f"[ERROR] Config not found: {config_path}")
|
||||
print(f" Create one from dhcpd.yaml.example")
|
||||
sys.exit(1)
|
||||
|
||||
cfg = Config(config_path)
|
||||
setup_logging(cfg.log_level, cfg.log_file)
|
||||
logging.getLogger("dhcpd").info("=" * 60)
|
||||
logging.getLogger("dhcpd").info("WinDHCPD starting")
|
||||
logging.getLogger("dhcpd").info("Config: %s", config_path)
|
||||
logging.getLogger("dhcpd").info("Scopes: %s", ", ".join(s.name for s in cfg.scopes))
|
||||
logging.getLogger("dhcpd").info("Bindings: %d", len(cfg.bindings))
|
||||
|
||||
# Lease database
|
||||
db_path = os.path.join(os.path.dirname(os.path.abspath(config_path)), "leases.db")
|
||||
db = LeaseDB(db_path)
|
||||
logging.getLogger("dhcpd").info("Lease DB: %s (%d leases loaded)",
|
||||
db_path, db.count())
|
||||
|
||||
# Server
|
||||
srv = DHCPd(cfg, db, iface=args.interface)
|
||||
srv.start()
|
||||
|
||||
# Web panel
|
||||
if not args.no_web:
|
||||
web_port = args.web_port or cfg.web_port
|
||||
web_host = cfg.web_host
|
||||
web_app.init(srv, cfg, db)
|
||||
t = threading.Thread(
|
||||
target=web_app.run,
|
||||
args=(web_host, web_port),
|
||||
daemon=True,
|
||||
name="web-panel",
|
||||
)
|
||||
t.start()
|
||||
logging.getLogger("dhcpd").info("Web panel: http://%s:%d", web_host, web_port)
|
||||
|
||||
# Graceful shutdown
|
||||
def shutdown(sig, frame):
|
||||
logging.getLogger("dhcpd").info("Received signal %s, shutting down...", sig)
|
||||
srv.stop()
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, shutdown)
|
||||
signal.signal(signal.SIGTERM, shutdown)
|
||||
|
||||
logging.getLogger("dhcpd").info("Server running. Press Ctrl+C to stop.")
|
||||
# Keep main thread alive
|
||||
try:
|
||||
while True:
|
||||
import time as _t
|
||||
_t.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
shutdown(signal.SIGINT, None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user