#!/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()