pydhcp is an open-source IPv4 DHCP server written in Python. Since isc-dhcp-server reached End-of-Life (EOL) in 2022, pydhcp aims to preserve many of its familiar features and configuration style for anyone looking to migrate, offering a friendly, similar-feeling alternative rather than a full replacement. It implements RFC 2131 over UDP 67/68, uses a compatible configuration syntax and lease file format under its own file paths, and runs as a native systemd service with an init.d wrapper included.
|
pydhcp es un servidor DHCP IPv4 de código abierto escrito en Python. Dado que isc-dhcp-server alcanzó su fin de vida (EOL) en 2022, pydhcp busca conservar muchas de sus características y estilo de configuración habituales para quienes quieran migrar, ofreciendo una alternativa amigable y similar, no un reemplazo completo. Implementa RFC 2131 sobre UDP 67/68, usa sintaxis de configuración y formato de concesiones compatible bajo sus propias rutas de archivo, y corre como servicio systemd nativo con wrapper init.d incluido.
|
📐 Runtime Architecture Diagram — visual walkthrough of the lease/reload pipeline.
- Python 3.8+
- systemd
- iproute2, gawk, passwd, util-linux, coreutils, grep, sed, iputils-ping, findutils, libc-bin, logrotate
Legend: ✅ same behaviour ·
⚠️ works, with a difference · ⛔ not implementedLeyenda: ✅ mismo comportamiento ·
⚠️ funciona, con una diferencia · ⛔ no implementado
| isc-dhcp-server | pydhcp | Description | Descripción |
|---|---|---|---|
| DHCPv4 (RFC 2131) over UDP 67/68 | ✅ | Python daemon, same protocol | Demonio Python, mismo protocolo |
| DHCPv6 | ⛔ | IPv4 only | Solo IPv4 |
| Multiple interfaces | ⛔ | Single interface, set in INTERFACESv4 |
Interfaz única, definida en INTERFACESv4 |
| BOOTP / PXE | ⛔ | Packets are padded to the BOOTP minimum for protocol compliance, but BOOTP clients are not served | Los paquetes se rellenan al mínimo BOOTP por cumplimiento del protocolo, pero no se atiende a clientes BOOTP |
| DHCP relay agents | ⛔ | No legitimate use without multi-segment support. giaddr/hops are parsed only to close a spoofing hole |
Sin uso legítimo sin soporte multi-segmento. giaddr/hops se parsean solo para cerrar un agujero de suplantación |
Clients whose chaddr differs from the frame source MAC |
⛔ | Dropped and logged: closes a spoofing hole | Descartados y registrados: cierra un agujero de suplantación |
| LDAP backend | ⛔ | Host reservations live in pydhcpd.conf, or in the mac-*.txt lists that pyleases.sh turns into reservations |
Las reservas de host viven en pydhcpd.conf, o en las listas mac-*.txt que pyleases.sh convierte en reservas |
| DDNS | ⛔ | The daemon never registers names in DNS; a lease grants an address, nothing else | El demonio nunca registra nombres en DNS; una concesión entrega una dirección, nada más |
client-updates / deny client-updates |
⛔ | Depends on DDNS plus the client FQDN option, neither implemented | Depende de DDNS y de la opción FQDN del cliente, ninguna implementada |
| isc-dhcp-server | pydhcp | Description | Descripción |
|---|---|---|---|
authoritative; |
✅ | pydhcpd's default, accepted for compatibility | Es el valor por defecto de pydhcpd, se acepta por compatibilidad |
not authoritative; |
✅ | The only way to disable authoritative mode | Única forma de desactivar el modo autoritativo |
server-identifier IP; |
✅ | Mandatory: the daemon refuses to start without it | Obligatoria: el demonio no arranca sin ella |
cleanup-interval N; |
✅ | Seconds between expired-lease sweeps | Segundos entre barridos de concesiones expiradas |
abandon-lease-time N; |
✅ | Seconds an IP is held out of the pool after a DHCPDECLINE | Segundos que una IP queda fuera del pool tras un DHCPDECLINE |
deny duplicates; |
✅ | Re-offers the MAC's existing lease on DISCOVER | Reofrece la concesión existente de la MAC en el DISCOVER |
deny declines; |
✅ | Ignores DHCPDECLINE messages | Ignora mensajes DHCPDECLINE |
ping-check true|false; |
✅ | Enabled by default in the shipped config, unlike isc | Activado por defecto en la configuración que se entrega, al revés que isc |
ping-timeout N; |
✅ | Seconds to wait for the ICMP reply before sending the OFFER | Segundos de espera de la respuesta ICMP antes de enviar el OFFER |
subnet ... { pool { ... } } |
✅ | Several pool { } blocks per subnet are accepted |
Se aceptan varios bloques pool { } por subred |
host NAME { hardware ethernet MAC; fixed-address IP; } |
✅ | fixed-address validated at config load and on SIGHUP |
fixed-address se valida al cargar la configuración y en cada SIGHUP |
class / subclass / deny members of |
✅ | Any class name is accepted, not just blockdhcp |
Se acepta cualquier nombre de clase, no solo blockdhcp |
allow|deny unknown-clients, known-clients |
✅ | Pool-level admission rules | Reglas de admisión a nivel de pool |
min-lease-time, default-lease-time, max-lease-time |
✅ | Validated together (0 < min <= default <= max), at subnet and pool level |
Se validan en conjunto (0 < min <= default <= max), a nivel de subred y de pool |
option routers |
Accepts a comma-separated list but uses only the first address; the rest are ignored with a notice. No failover between routers | Acepta lista separada por comas pero usa solo la primera dirección; el resto se ignora con aviso. No hay conmutación entre routers | |
option broadcast-address |
✅ | Served as option 28 | Se entrega como opción 28 |
option domain-name-servers |
✅ | Comma-separated list, all served | Lista separada por comas, se entregan todas |
option wpad ...; |
✅ | Option 252, gated by WPAD_ENABLED in pydhcp.env |
Opción 252, condicionada por WPAD_ENABLED en pydhcp.env |
one-lease-per-client |
Always enforced, never declared: a MAC never holds more than one pool lease | Siempre aplicado, nunca declarado: una MAC nunca tiene más de una concesión del pool | |
option domain-name |
⛔ | Option 15, the DNS search domain. Validating it would mean resolving a name at config load, which this daemon never does, and a search domain often has no record of its own | Opción 15, el dominio de búsqueda DNS. Validarla exigiría resolver un nombre al cargar la configuración, cosa que este demonio nunca hace, y un dominio de búsqueda a menudo no tiene registro propio |
option subnet-mask override |
⛔ | The netmask sent always matches the subnet ... netmask ... declaration |
La máscara enviada siempre coincide con la declaración subnet ... netmask ... |
| Per-host / per-class option scoping | ⛔ | Options declared at subnet level apply to every client uniformly |
Las opciones declaradas a nivel subnet aplican a todos los clientes por igual |
| isc-dhcp-server | pydhcp | Description | Descripción |
|---|---|---|---|
/etc/dhcp/dhcpd.conf |
/etc/pydhcp/core/pydhcpd.conf |
Same syntax | Misma sintaxis |
/var/lib/dhcp/dhcpd.leases |
/etc/pydhcp/core/pydhcpd.leases |
Same format | Mismo formato |
/etc/default/isc-dhcp-server |
/etc/pydhcp/pydhcp.env |
Bootstrap values, plus pydhcp's own extras | Valores de arranque, más los extras propios de pydhcp |
/run/dhcp-server/dhcpd.pid |
/run/pydhcp/pydhcpd.pid |
Fixed path, not configurable | Ruta fija, no configurable |
/etc/systemd/system/isc-dhcp-server.service |
/etc/systemd/system/pydhcpd.service |
pydhcp starts already unprivileged (User=pydhcpd); isc starts as root and drops privileges itself |
pydhcp arranca ya sin privilegios (User=pydhcpd); isc arranca como root y baja de privilegios él mismo |
/etc/init.d/isc-dhcp-server |
/etc/init.d/pydhcpd |
Compatible wrapper | Wrapper compatible |
/var/log/syslog |
/var/log/pydhcp.log |
Writes directly to file, does not use syslog, so no log-facility directive is needed |
Escribe directamente al archivo, no usa syslog, así que no hace falta la directiva log-facility |
/etc/logrotate.d/rsyslog |
/etc/logrotate.d/pydhcp |
Daily, one config for the whole project | Diaria, una sola configuración para todo el proyecto |
journalctl -u isc-dhcp-server |
journalctl -u pydhcpd |
Both also reach journald through systemd | Ambos llegan además a journald a través de systemd |
| isc-dhcp-server | pydhcp | Description | Descripción |
|---|---|---|---|
systemctl start|stop|restart|status isc-dhcp-server |
systemctl start|stop|restart|status pydhcpd |
Same verbs, same behaviour | Mismos verbos, mismo comportamiento |
service isc-dhcp-server start|stop|restart|status |
service pydhcpd start|stop|restart|status |
Goes through the init.d wrapper, which delegates to systemctl when systemd is the active init |
Pasa por el wrapper de init.d, que delega en systemctl cuando systemd es el init activo |
dhcpd -t -cf /etc/dhcp/dhcpd.conf |
pydhcpd.py -t -cf /etc/pydhcp/core/pydhcpd.conf |
Config syntax test | Prueba de sintaxis de la configuración |
| isc-dhcp-server | pydhcp | Description | Descripción |
|---|---|---|---|
DHCPREQUEST for 192.168.0.50 (192.168.0.2) from aa:bb:cc:dd:ee:ff via enpXsXDHCPACK on 192.168.0.50 to aa:bb:cc:dd:ee:ff (FOO) via enpXsX |
REQUEST from aa:bb:cc:dd:ee:ff (FOO)ACK aa:bb:cc:dd:ee:ff → 192.168.0.50(lease 2592000s) |
Authorized client with static IP (renewal) | Cliente autorizado con IP estática (renovación) |
DHCPDISCOVER from bb:cc:dd:ee:ff:aa via enpXsXDHCPOFFER on 192.168.0.230 to bb:cc:dd:ee:ff:aa (BAR) via enpXsXDHCPREQUEST for 192.168.0.230 (192.168.0.2) from bb:cc:dd:ee:ff:aa (BAR) via enpXsXDHCPACK on 192.168.0.230 to bb:cc:dd:ee:ff:aa (BAR) via enpXsX |
DISCOVER from bb:cc:dd:ee:ff:aa (BAR)OFFER bb:cc:dd:ee:ff:aa → 192.168.0.230REQUEST from bb:cc:dd:ee:ff:aa (BAR)ACK bb:cc:dd:ee:ff:aa → 192.168.0.230(lease 60s) |
Unknown client entering the block pool | Cliente desconocido ingresando al pool de bloqueo |
DHCPDISCOVER from bb:cc:dd:ee:ff:aa via enpXsX: network 192.168.0.0/24: no free leases |
DISCOVER from bb:cc:dd:ee:ff:aa (BAR)No IP for bb:cc:dd:ee:ff:aa -- skip |
Pool exhausted | Pool agotado |
DHCPDISCOVER from bb:cc:dd:ee:ff:aa via enpXsX: network 192.168.0.0/24: no free leases |
DISCOVER from bb:cc:dd:ee:ff:aa (BAR)Blocked bb:cc:dd:ee:ff:aa -- skip |
Blocked client — isc-dhcp-server reports it as a full pool |
Cliente bloqueado — isc-dhcp-server lo reporta como pool lleno |
A pool with
deny members of "blockdhcp"becomes invisible to that class, soisc-dhcp-serversees no lease available and logs the same message as a genuinely full pool.pydhcpdlogs the real cause:BlockedorNo IP.Un pool con
deny members of "blockdhcp"se vuelve invisible para esa clase, así queisc-dhcp-serverno ve ninguna concesión disponible y escribe el mismo mensaje que un pool genuinamente lleno.pydhcpdregistra la causa real:BlockedoNo IP.
| Configuration | isc-dhcp-server | pydhcpd | Description | Descripción |
|---|---|---|---|---|
authoritative; present |
authoritative | authoritative | ✅ same. In pydhcpd it is already the default, so the line changes nothing | ✅ igual. En pydhcpd ya es el valor por defecto, así que la línea no cambia nada |
not authoritative; present |
non-authoritative | non-authoritative | ✅ same syntax, same result | ✅ misma sintaxis, mismo resultado |
| neither directive present | non-authoritative | authoritative |
Migrating from isc-dhcp-server: if your
dhcpd.confdid not carryauthoritative;and you relied on that, addnot authoritative;explicitly to keep the old behavior. Everyone else needs to change nothing.Migrando desde isc-dhcp-server: si su
dhcpd.confno llevabaauthoritative;y usted dependía de eso, agreguenot authoritative;de forma explícita para conservar el comportamiento anterior. Los demás no necesitan cambiar nada.
| Event | isc-dhcp-server (as authoritative) | pydhcpd (as authoritative) |
|---|---|---|
| Rogue offers IP to client | (not observed) † | (not observed) † |
| Client requests rogue IP | DHCPREQUEST for 192.168.0.222 (192.168.0.249) from bb:cc:dd:ee:ff:aa (BAR) via enpXsX |
REQUEST from bb:cc:dd:ee:ff:aa (BAR) |
| Rogue acknowledges | (not observed) † | (not observed) † |
| Authoritative server rejects | DHCPNAK on 192.168.0.222 to bb:cc:dd:ee:ff:aa via enpXsX |
NAK → bb:cc:dd:ee:ff:aa(Not authorized for this IP) |
| Client rediscovers | DHCPDISCOVER from bb:cc:dd:ee:ff:aa via enpXsX |
DISCOVER from bb:cc:dd:ee:ff:aa (BAR) |
† Neither daemon sees these packets:
DHCPOFFER/DHCPACKare addressed to the client, not to other DHCP servers on the segment.† Ninguno de los dos demonios ve esos paquetes:
DHCPOFFER/DHCPACKvan dirigidos al cliente, no a otros servidores DHCP del segmento.
| isc-dhcp-server | pydhcpd |
|---|---|
Has no built-in per-client rate-limiting for lease allocation. Abuse mitigation relies on deny duplicates;, plus pool exhaustion (once the pool is full, further DHCPDISCOVER messages simply receive no DHCPOFFER). Client identification is based on chaddr (or client-id, option 61) — never on the Ethernet source MAC of the frame, so behavior is identical whether the client is directly attached or behind a relay (giaddr is only used for routing the reply).No tiene rate-limiting incorporado por cliente para la asignación de leases. La mitigación de abuso depende de deny duplicates;, además del agotamiento del pool (una vez lleno, los DHCPDISCOVER simplemente no reciben DHCPOFFER). La identificación del cliente se basa en chaddr (o client-id, opción 61) — nunca en la MAC Ethernet origen del frame, por lo que el comportamiento es igual si el cliente está conectado directamente o detrás de un relay (giaddr solo se usa para enrutar la respuesta). |
Adds a sliding-window rate limit on lease allocation, keyed by client MAC (chaddr) — the same identifier isc-dhcp-server uses. Each client MAC has its own bucket, so multiple clients behind the same relay are rate-limited independently and do not affect each other. If a single MAC exceeds the allowed number of allocations within the window, further requests are rejected with reason "rate limited" until the window slides forward. This is purely an internal safeguard against allocation storms; it is not configurable via pydhcpd.conf and has no equivalent directive in isc-dhcp-server.Agrega un límite de tasa (sliding window) sobre la asignación de leases, indexado por MAC del cliente ( chaddr) — el mismo identificador que usa isc-dhcp-server. Cada MAC de cliente tiene su propio cupo, de modo que varios clientes detrás del mismo relay se limitan de forma independiente y no se afectan entre sí. Si una MAC supera el número de asignaciones permitidas dentro de la ventana, las solicitudes adicionales se rechazan con la razón "rate limited" hasta que la ventana avance. Esto es solo una salvaguarda interna contra ráfagas de asignación; no es configurable desde pydhcpd.conf y no tiene directiva equivalente en isc-dhcp-server.Note that, unlike isc-dhcp-server, pydhcpd does look at the Ethernet source MAC — not to identify the client, but as a drop filter: a non-relayed packet whose chaddr does not match it is discarded (see Scope).A diferencia de isc-dhcp-server, pydhcpd sí mira la MAC Ethernet origen — no para identificar al cliente, sino como filtro de descarte: un paquete no relayado cuyo chaddr no coincida con ella se descarta (ver Scope). |
known limitation, both servers: neither controls an attacker who rotates MAC addresses to exhaust the pool.
isc-dhcp-server's gap is total — it has no per-client throttle at all, so a plain flood from a single MAC already drains the pool, no rotation needed.pydhcpd's gap is narrower: its per-MAC limit stops a single-MAC flood, but is keyed by MAC (chaddr), so it only bounds how fast one MAC can allocate — it does not cap the total across many different MACs, and an attacker rotating MACs can still drain the pool one new MAC at a time. A global (cross-MAC) rate limit was considered and intentionally left out ofpydhcpd: it would need careful tuning to avoid rejecting legitimate clients during a normal burst of reconnections (e.g. many devices rejoining after a power outage), and there is no evidence MAC-rotation abuse is a live threat worth that trade-off. Documented here as a known, accepted limitation.limitación conocida, en ambos servidores: ninguno controla a un atacante que rota direcciones MAC para agotar el pool. La brecha de
isc-dhcp-serveres total — no tiene ningún control por cliente, así que una inundación simple desde una sola MAC ya agota el pool, sin necesidad de rotar. La brecha depydhcpdes más acotada: su límite por MAC frena la inundación de una sola MAC, pero está indexado por MAC (chaddr), así que solo acota qué tan rápido puede asignar una MAC — no limita el total entre muchas MACs distintas, y un atacante que rote MACs igual puede vaciar el pool, una MAC nueva a la vez. Se evaluó un límite global (entre todas las MACs) parapydhcpdy se dejó afuera intencionalmente: requeriría un ajuste cuidadoso para no rechazar clientes legítimos durante una ráfaga normal de reconexiones (p.ej. varios dispositivos reconectándose tras un corte de luz), y no hay evidencia de que el abuso por rotación de MAC sea una amenaza activa que justifique ese costo. Se documenta acá como una limitación conocida y aceptada.
| isc-dhcp-server | pydhcpd |
|---|---|
| Does not limit how many pool IPs a single MAC can accumulate | Always limits a MAC to a single pool IP at a time; fixed daemon behavior, not a configurable directive |
| No tiene restricción sobre cuántas IPs del pool puede acumular una MAC | Siempre limita a una MAC a una sola IP del pool a la vez; comportamiento fijo del demonio, no es una directiva configurable |
| isc-dhcp-server | pydhcpd |
|---|---|
Supports scoping any option — including option wpad (252) — at multiple levels: subnet, class/subclass, or an individual host. A more specific scope overrides a broader one, so an admin can declare WPAD at the subnet level for every client and then override or omit it for a specific class or host (e.g. exclude a group of trusted/unrestricted devices from the PAC).Soporta el alcance de cualquier opción — incluyendo option wpad (252) — en varios niveles: subnet, class/subclass, o un host individual. Un alcance más específico sobreescribe uno más amplio, así que un administrador puede declarar WPAD a nivel subnet para todos los clientes y luego sobreescribirlo u omitirlo para una class o host específico (ej. excluir a un grupo de dispositivos confiables/sin restricción del PAC). |
Has no option-scoping mechanism at all — config.wpad_url is a single global value read once from the subnet block, applied identically to every OFFER/ACK/INFORM it sends. WPAD_ENABLED in pydhcp.env is therefore all-or-nothing: on turns WPAD on for every client, off turns it off for every client. The existing class "blockdhcp"/subclass mechanism does not generalize to this — it only marks MACs for lease denial, not a scoping construct for arbitrary options like isc-dhcp-server's classes.No tiene ningún mecanismo de alcance de opciones — config.wpad_url es un único valor global leído una vez del bloque subnet, aplicado igual a cada OFFER/ACK/INFORM que envía. WPAD_ENABLED en pydhcp.env es entonces todo-o-nada: activado prende WPAD para todos los clientes, desactivado lo apaga para todos. El mecanismo existente de class "blockdhcp"/subclass no generaliza a este caso — solo marca MACs para negarles el lease, no un constructo de alcance para opciones arbitrarias como sí lo son las clases de isc-dhcp-server. |
Workaround (external to pydhcp): if
WPAD_ENABLED=trueand some MACs must never see the PAC, block their access to the PAC's port (e.g. 18100) at the firewall. This does not stoppydhcpdfrom sending option 252 to them, but the client can never fetch the PAC file, and the PAC's ownDIRECTfallback lets it proceed without a proxy. This is a firewall-side workaround, not apydhcpfeature —pydhcphas no firewall component and does not ship or manage this rule itself.Workaround (externo a pydhcp): si
WPAD_ENABLED=truey algunas MACs nunca deben ver el PAC, bloquee su acceso al puerto del PAC (ej. 18100) en el firewall. Esto no evita quepydhcpdles mande la opción 252, pero el cliente nunca podrá descargar el archivo PAC, y el fallbackDIRECTdel propio PAC le permite seguir sin proxy. Este es un workaround del lado del firewall, no una funcionalidad depydhcp—pydhcpno tiene componente de firewall y no provee ni gestiona esa regla.
| isc-dhcp-server | pydhcp | Description | Descripción |
|---|---|---|---|
| ⛔ | ✅ | Per-MAC allocation rate limit: 5 allocations per minute per MAC | Límite de asignación por MAC: 5 asignaciones por minuto y por MAC |
| ⛔ | ✅ | DISCOVER reservations: a DISCOVER earns a 30s in-memory reservation, never a full lease, so a flood cannot hold pool IPs | Reservas por DISCOVER: un DISCOVER obtiene una reserva en memoria de 30s, nunca una concesión completa, así que una inundación no puede retener IPs del pool |
| ⛔ | ✅ | Corrupt lease cleanup at startup: malformed entries are removed from the leases file | Limpieza de concesiones corruptas al arrancar: las entradas malformadas se eliminan del archivo |
| ⛔ | ✅ | ACL automation: pyleases.sh rebuilds pydhcpd.conf from MAC lists |
Automatización de ACL: pyleases.sh reconstruye pydhcpd.conf desde listas de MAC |
pydhcp/
├── acl/
│ └── blockdhcp.txt # MAC addresses blocked from getting a DHCP lease
│
├── core/
│ ├── pydhcpd.py # DHCP daemon: handles DISCOVER/OFFER/REQUEST/ACK
│ ├── pydhcpd.conf # Daemon config: hosts, pools, lease timers
│
├── init.d/
│ └── pydhcpd # init.d-style start/stop/status wrapper around the systemd service
│
├── service/
│ └── pydhcpd.service # systemd unit that runs the daemon
│
├── tools/
│ ├── bkstack.sh # Backs up pydhcp and uhm configuration and data (see Tools section)
│ ├── pyleases.sh # Rebuilds pydhcpd.conf from ACL files and manages leases (see Tools section)
│ └── pywebmin.sh # Installs the Webmin module for managing pydhcpd from the browser (see Tools section)
└── pysetup.sh # Installs, updates or removes pydhcp
| Files generated at runtime (not included in the repository): | Archivos generados en runtime (no incluidos en el repositorio): |
/etc/pydhcp/core/pydhcpd.leases # Active leases database
/run/pydhcp/pydhcpd.pid # PID file, written by the daemon
# (systemd creates the directory)
/etc/pydhcp/pydhcp.env.bak # pydhcp.env rollback copy by pyleases.sh before adding keys, 1 kept
/etc/bak/pydhcp/bkstack_<TIMESTAMP>.zip # Full project backup written by tools/bkstack.sh, up to 3 kept
/etc/pydhcp/core/pydhcpd.conf.webmin.bak # Rollback copy written by the Webmin module (pywebmin.sh) on each save, 1 kept
/etc/webmin/pydhcp/.csrf_token # CSRF secret for the Webmin module (pywebmin.sh), mode 0600
| Clone the repository and run the installer to deploy all files to their correct system paths: | Clona el repositorio y ejecuta el instalador para desplegar todos los archivos en sus rutas de sistema correctas: |
git clone --depth=1 https://github.com/maravento/pydhcp.git
cd pydhcp
sudo bash pysetup.sh| To update or remove pydhcp, download the updated repository, enter the repository folder and run: | Para actualizar o eliminar pydhcp, descargar el repositorio actualizado, ingresar a la carpeta del repositorio y ejecutar: |
cd pydhcp
sudo bash pysetup.sh --update
# or
sudo bash pysetup.sh --remove| File | --update |
--remove |
|---|---|---|
pydhcpd.py |
✅ overwritten | ✅ removed |
pydhcpd.service |
✅ overwritten | ✅ removed |
init.d/pydhcpd |
✅ overwritten | ✅ removed |
tools/bkstack.sh |
✅ overwritten | ✅ removed (its cron entry is deregistered first) |
tools/pyleases.sh |
✅ overwritten | ✅ removed |
tools/pywebmin.sh |
✅ overwritten | ✅ removed (also uninstalls the Webmin module, if installed) |
pydhcpd.conf |
⛔ preserved | ✅ removed |
pydhcpd.leases |
⛔ preserved | ✅ removed |
pydhcp.env |
⛔ preserved | ✅ removed |
/var/log/pydhcp.log (shared by the daemon, pysetup.sh and tools/pyleases.sh) |
⛔ preserved | ✅ removed |
/etc/logrotate.d/pydhcp |
⛔ preserved | ✅ removed |
system user/group pydhcpd |
⛔ preserved | ✅ removed |
acl/blockdhcp.txt (pydhcp's own block list) |
⛔ preserved | ✅ removed |
pydhcp.env.bak, core/pydhcpd.conf.bak, core/pydhcpd.conf.webmin.bak (rollback copies) |
⛔ preserved | ✅ removed |
/etc/bak/ (project backups written by tools/bkstack.sh) |
⛔ preserved | ⛔ preserved |
/etc/acl/mac/ (administrator's own ACL lists) |
⛔ preserved | ⛔ preserved |
/etc/aclis never touched by--remove. It holds the administrator's ownmac-*.txtlists, edited by hand, whichpydhcpmay or may not use depending on whether the optionaltools/pyleases.shtool is ever run —pysetup.shcreates the directory regardless, so uninstalling the daemon does not assume that data is safe to discard.
/etc/aclnunca se toca en--remove. Contiene las listasmac-*.txtpropias del administrador, editadas a mano, quepydhcppuede o no usar según si la herramienta opcionaltools/pyleases.shllega a ejecutarse —pysetup.shcrea el directorio de todos modos, así que desinstalar el demonio no asume que esos datos sean seguros de descartar.
blockdhcp.txtis a different case: it ispydhcp's own list, written bypyleases.shalone and never edited by hand, so it lives under/etc/pydhcp/acl/— the same arrangementuhmuses for its own lists under/etc/uhm/acl/.--removedeletes it along with the rest of/etc/pydhcp; runtools/bkstack.shfirst if you want a copy.
blockdhcp.txtes un caso distinto: es la lista propia depydhcp, escrita solo porpyleases.shy nunca editada a mano, así que vive bajo/etc/pydhcp/acl/— la misma disposición que usauhmpara sus propias listas bajo/etc/uhm/acl/.--removela borra junto con el resto de/etc/pydhcp; ejecutetools/bkstack.shantes si quiere una copia.
# Edit main config | Editar configuración principal
sudo nano /etc/pydhcp/core/pydhcpd.conf
# Restart service | Reiniciar servicio
sudo systemctl restart pydhcpd
# Check status | Verificar estado
sudo systemctl status pydhcpd
# ● pydhcpd.service - pydhcpd - Python DHCP Daemon
# Loaded: loaded (/etc/systemd/system/pydhcpd.service; enabled; preset: enabled)
# Active: active (running) since Tue 2026-06-09 17:51:49 -05; 17s ago
# Docs: https://github.com/maravento/pydhcp
# Main PID: 2356158 (python3)
# Tasks: 3 (limit: 76240)
# Memory: 11.0M (peak: 11.6M)
# CPU: 331ms
# CGroup: /system.slice/pydhcpd.service
# └─2356158 /usr/bin/python3 /etc/pydhcp/core/pydhcpd.py
# jun 09 17:51:49 host systemd[1]: Started pydhcpd.service - pydhcpd - Python DHCP Daemon.
# jun 09 17:51:49 host python3[1411247]: 2026-06-09 17:51:49,068 INFO: Attached BPF filter to raw socket (dst port 67)
# jun 09 17:51:49 host python3[1449863]: 2026-06-09 16:20:31,317 INFO: Config loaded: 158 hosts, 208 blocked
# jun 09 17:51:49 host python3[1449863]: 2026-06-09 16:20:31,323 INFO: Leases loaded: 2 entries
# jun 09 17:51:49 host python3[1411247]: 2026-06-09 17:51:49,068 INFO: Listening on enpXsX (DHCP port 67)
# Other entries...
# jun 09 17:51:49 host python3[2356158]: 2026-06-09 17:51:49,071 INFO: pydhcpd started (pid 2356158)
# jun 09 17:51:49 host python3[2356158]: 2026-06-09 17:51:49,071 INFO: interface enpXsX
# jun 09 17:51:49 host python3[2356158]: 2026-06-09 17:51:49,072 INFO: Listening on enpXsX (DHCP port 67)
# jun 09 17:51:52 host python3[2356158]: 2026-06-09 17:51:52,316 INFO: DISCOVER from aa:bb:cc:dd:ee:ff (FooBar)
# jun 09 17:51:52 host python3[2356158]: 2026-06-09 17:51:52,316 INFO: Blocked aa:bb:cc:dd:ee:ff -- skip
# jun 09 17:52:02 host python3[2356158]: 2026-06-09 17:52:02,086 INFO: DISCOVER from bb:cc:dd:ee:ff:aa (<no hostname>)
# jun 09 17:52:02 host python3[2356158]: 2026-06-09 17:52:02,154 INFO: OFFER bb:cc:dd:ee:ff:aa → 192.168.0.231
# jun 09 17:52:02 host python3[2356158]: 2026-06-09 17:52:02,264 INFO: REQUEST from bb:cc:dd:ee:ff:aa (<no hostname>)
# jun 09 17:52:02 host python3[2356158]: 2026-06-09 17:52:02,283 INFO: ACK bb:cc:dd:ee:ff:aa → 192.168.0.231
# jun 09 17:52:02 host python3[2356158]: 2026-06-09 17:52:02,283 INFO: (lease 60s)
# jun 09 17:52:15 host python3[2356158]: 2026-06-09 17:52:15,391 INFO: DISCOVER from cc:dd:ee:ff:aa:bb (BazHost)
# jun 09 17:52:15 host python3[2356158]: 2026-06-09 17:52:15,391 INFO: No IP for cc:dd:ee:ff:aa:bb -- skip
# jun 09 17:53:02 host python3[2356158]: 2026-06-09 17:53:02,173 INFO: Lease expired: 192.168.0.230
# View active leases | Ver concesiones activas
cat /etc/pydhcp/core/pydhcpd.leases
# Reload config without restart (SIGHUP) | Recargar configuración sin reiniciar (SIGHUP)
sudo systemctl reload pydhcpd
# Test configuration syntax without starting the daemon (-t [-cf FILE])
sudo /etc/pydhcp/core/pydhcpd.py --test
sudo /etc/pydhcp/core/pydhcpd.py -t -cf /path/to/alternate.conf
# View logs (journald) | Ver logs (journald)
sudo journalctl -u pydhcpd -f
# View logs (file) | Ver logs (archivo)
sudo tail -f /var/log/pydhcp.log| The installer configures the interface, server IP, subnet, pool range, and DNS servers interactively. After installation, edit the configuration file only to add static host reservations or blocked MACs. Then restart the service to apply changes. | El instalador configura la interfaz, IP del servidor, subred, rango del pool y servidores DNS de forma interactiva. Tras la instalación, edita el archivo de configuración solo para agregar reservas estáticas o MACs bloqueadas. Luego reinicia el servicio para aplicar los cambios. |
| File | Description | Descripción |
|---|---|---|
/etc/pydhcp/core/pydhcpd.conf |
Main configuration file | Archivo de configuración principal |
/etc/pydhcp/core/pydhcpd.leases |
Active leases database | Base de datos de concesiones activas |
/etc/pydhcp/pydhcp.env |
Shared config: daemon defaults (config/pid/leases paths, interface, user/group), network values, ACL paths, lease timers and WPAD/ping-check flags — all generated by pysetup.sh at install time; tools/pyleases.sh only adds any of these that are still missing (e.g. after an update from an older pysetup.sh) |
Configuración compartida: defaults del demonio (rutas de config/pid/leases, interfaz, usuario/grupo), valores de red, rutas ACL, temporizadores de lease y flags de WPAD/ping-check — todos generados por pysetup.sh durante la instalación; tools/pyleases.sh solo agrega los que falten (p.ej. tras una actualización desde un pysetup.sh anterior) |
/etc/systemd/system/pydhcpd.service |
systemd unit | Unidad systemd |
/etc/init.d/pydhcpd |
init.d wrapper | Wrapper init.d |
pydhcp.env holds only bootstrap values -- paths, the interface, and the daemon's user/group -- read once by pydhcpd.py at startup. Every lease timer, ping-check, ping-timeout, abandon-lease-time, WPAD and static host/block-list entry is a pydhcpd.conf directive, never a pydhcp.env key.
|
pydhcp.env contiene solo valores de arranque -- rutas, la interfaz y el usuario/grupo del demonio -- que pydhcpd.py lee una vez al iniciar. Todo temporizador de concesión, ping-check, ping-timeout, abandon-lease-time, WPAD y entrada de host estático o de lista de bloqueo es una directiva de pydhcpd.conf, nunca una clave de pydhcp.env.
|
| Variable | Description | Descripción |
|---|---|---|
INTERFACESv4 |
Interface pydhcpd.py listens on |
Interfaz en la que escucha pydhcpd.py |
DAEMON_USER, DAEMON_GROUP |
Owner the daemon sets on the leases file it rewrites. The process itself is started already unprivileged by pydhcpd.service, so it never drops privileges |
Propietario que el demonio pone al archivo de concesiones que reescribe. El proceso lo arranca ya sin privilegios pydhcpd.service, así que nunca baja de privilegios |
DHCPDv4_CONF |
Path to pydhcpd.conf, read at startup and on SIGHUP/reload |
Ruta a pydhcpd.conf, leída al arrancar y en SIGHUP/reload |
DHCPDv4_BIN, DHCPDv4_SCRIPT |
Python interpreter and daemon script path, used by init.d/pydhcpd and pywebmin.sh to invoke pydhcpd.py for config tests |
Intérprete Python y ruta del script del demonio, usados por init.d/pydhcpd y pywebmin.sh para invocar pydhcpd.py en las pruebas de configuración |
PYDHCPD_LEASES |
Leases database path | Ruta de la base de datos de leases |
A second, distinct group in pydhcp.env: features with no pydhcpd.conf directive behind them, so there is nothing to keep in sync with a pydhcpd.conf template -- pydhcpd.py reads them directly from pydhcp.env at startup, the same way it reads the bootstrap group above. pyleases.sh never touches these; they only build pydhcpd.conf, and these values aren't pydhcpd.conf directives.
|
Un segundo grupo, distinto del anterior, dentro de pydhcp.env: funciones que no tienen detrás una directiva de pydhcpd.conf, así que no hay nada que mantener sincronizado con una plantilla. pydhcpd.py las lee directamente de pydhcp.env al arrancar, igual que el grupo de arranque de arriba. pyleases.sh nunca las toca: solo construye pydhcpd.conf, y estos valores no son directivas de ese archivo.
|
| Variable | Description | Descripción |
|---|---|---|
PING_CACHE_TTL_SECONDS |
Seconds a ping-check result (alive/dead) is cached before re-checking the same IP; default 120 |
Segundos que se cachea el resultado de un ping-check (viva/muerta) antes de re-verificar la misma IP; default 120 |
RATE_LIMIT_WINDOW_SECONDS, RATE_LIMIT_MAX |
Anti-abuse throttle: at most RATE_LIMIT_MAX new lease allocations per MAC within RATE_LIMIT_WINDOW_SECONDS, to limit pool exhaustion by an attacker rotating MACs; defaults 60/5 |
Freno anti-abuso: como máximo RATE_LIMIT_MAX asignaciones de lease nuevas por MAC dentro de RATE_LIMIT_WINDOW_SECONDS, para limitar el agotamiento del pool por un atacante rotando MACs; defaults 60/5 |
RESERVATION_TTL_SECONDS |
Seconds a DISCOVER-only provisional reservation holds an IP before expiring, if no matching REQUEST follows; default 30 |
Segundos que una reserva provisional de un DISCOVER retiene una IP antes de expirar, si no llega el REQUEST correspondiente; default 30 |
All four values above must be at least 1. A value below 1 (for example RATE_LIMIT_MAX=0, which does not mean "unlimited") is rejected with a WARNING in the log and the default is used instead.
Los cuatro valores anteriores deben ser como mínimo 1. Un valor menor que 1 (por ejemplo RATE_LIMIT_MAX=0, que no significa "sin límite") se rechaza con un WARNING en el log y en su lugar se usa el valor por defecto.
A third group in pydhcp.env, distinct from the two above: input values for pyleases.sh's optional automation layer. Unlike the bootstrap group, pydhcpd.py never reads these directly — pysetup.sh creates them and pyleases.sh writes the corresponding directive into pydhcpd.conf on every run (see Supported directives); a bare install managed by hand never needs them. Any missing key is added by pyleases.sh itself, with its own built-in default, right before the file's closing # =====...===== line — this only happens on an install that predates a given key.
|
Un tercer grupo dentro de pydhcp.env, distinto de los dos anteriores: valores de entrada para la capa opcional de automatización de pyleases.sh. A diferencia del grupo de arranque, pydhcpd.py nunca los lee directamente — pysetup.sh los crea y pyleases.sh escribe la directiva correspondiente en pydhcpd.conf en cada ejecución; una instalación gestionada a mano nunca los necesita. Cualquier clave que falte la agrega el propio pyleases.sh, con su valor por defecto, justo antes de la línea de cierre # =====...===== del archivo; eso solo ocurre en una instalación anterior a esa clave.
|
| Variable | pydhcpd.conf directive it becomes |
Description | Descripción |
|---|---|---|---|
CLEANUP_INTERVAL |
cleanup-interval |
Pool cleanup frequency in seconds; default 60 |
Frecuencia de limpieza del pool en segundos; default 60 |
AUTHORIZED_LEASE_TIME |
subnet min/default/max-lease-time |
Lease duration for authorized/static clients in seconds; default 2592000 (30 days) |
Duración del lease para clientes autorizados/estáticos en segundos; default 2592000 (30 días) |
QUARANTINE_DURATION |
abandon-lease-time |
See "IP quarantine" in Operational Details below; default 60 |
Ver "IP quarantine" en Operational Details abajo; default 60 |
WPAD_ENABLED |
option wpad ...; |
See WPAD/PAC via DHCP option 252 below; default false |
Ver WPAD/PAC via DHCP option 252 abajo; default false |
WPAD_PORT |
port inside the option wpad ...; URL |
TCP port of the Apache VirtualHost serving wpad.pac; default 18100. Only asked for at install time when apache2 is already installed and WPAD is accepted |
Puerto TCP del VirtualHost de Apache que sirve wpad.pac; default 18100. Solo se pregunta durante la instalación si apache2 ya está instalado y se acepta WPAD |
PING_CHECK_ENABLED |
ping-check |
See "ping-check" in Operational Details below; default true |
Ver "ping-check" en Operational Details abajo; default true |
PING_TIMEOUT_SECONDS |
ping-timeout |
See "ping-check" in Operational Details below; default 1 |
Ver "ping-check" en Operational Details abajo; default 1 |
pydhcp.env only ever holds real, admin-adjustable values -- the two groups above, plus the bootstrap group and the pyleases.sh input group described earlier. A handful of internal constants in pydhcpd.py are deliberately **not** exposed in pydhcp.env (or pydhcpd.conf), because each is a protocol/math invariant or an internal implementation detail with no admin-meaningful range of alternatives -- not an operational choice to make:
|
pydhcp.env solo contiene valores reales, ajustables por el administrador: los dos grupos de arriba, más el grupo de arranque y el de entrada de pyleases.sh descritos antes. Un puñado de constantes internas de pydhcpd.py queda deliberadamente fuera de pydhcp.env (y de pydhcpd.conf), porque cada una es un invariante del protocolo o de la aritmética, o un detalle interno de implementación sin un rango de alternativas con sentido para el administrador; no es una decisión operativa:
|
| Value | Why it's fixed | Por qué es fijo |
|---|---|---|
Max pool/subnet size (65536 addresses) |
Fixed by IPv4 arithmetic, not a policy -- a /16 is the largest a range can ever be |
Fijado por la aritmética de IPv4, no es una política -- un /16 es el rango más grande que puede existir |
Max DHCP option length (255 bytes -- WPAD URL and other option values) |
Fixed by the 1-byte length field in the DHCP option format (RFC 2132); there's no larger value the protocol can even represent | Fijado por el campo de longitud de 1 byte del formato de opción DHCP (RFC 2132); no hay un valor mayor que el protocolo pueda siquiera representar |
Allocation round-robin counter wraparound (2**16) |
Internal iteration state with no observable effect on behavior -- changing it doesn't change what the daemon does | Estado de iteración interno sin efecto observable en el comportamiento -- cambiarlo no cambia lo que hace el demonio |
Main-loop shutdown poll timeout (5s socket timeout) |
Controls how fast a systemctl stop is noticed, not DHCP behavior (leases, OFFERs, etc.) |
Controla qué tan rápido se nota un systemctl stop, no el comportamiento DHCP (leases, OFFERs, etc.) |
The daemon runs as the system account pydhcpd, not as root, with two kernel capabilities granted by pydhcpd.service: CAP_NET_RAW (raw socket and ICMP ping-check) and CAP_NET_BIND_SERVICE (bind port 67). No other capability is needed or granted. Ownership is therefore assigned by what the daemon does with each file, not uniformly. These values are set by pysetup.sh and are deliberate — the table documents the reasoning so it does not have to be re-derived. They are also enforced afterwards: on every run, tools/pyleases.sh checks pydhcp.env, pydhcpd.conf, pydhcpd.leases, the ACL lists and /var/log/pydhcp.log, and restores exactly these owners and modes if any of them was changed, logging a WARNING.
|
El demonio corre bajo la cuenta de sistema pydhcpd, no como root, con dos capacidades del kernel concedidas por pydhcpd.service: CAP_NET_RAW (socket crudo y ping-check ICMP) y CAP_NET_BIND_SERVICE (escuchar en el puerto 67). No necesita ni recibe ninguna otra. Por eso el propietario se asigna según qué hace el demonio con cada archivo, no de forma uniforme. Estos valores los aplica pysetup.sh y son deliberados — la tabla documenta el porqué para no tener que deducirlo otra vez. También se hacen cumplir después: en cada ejecución, tools/pyleases.sh comprueba pydhcp.env, pydhcpd.conf, pydhcpd.leases, las listas ACL y /var/log/pydhcp.log, y restablece exactamente estos propietarios y modos si alguno fue alterado, registrando un WARNING.
|
| Path | Owner | Mode | What the daemon does | Why |
|---|---|---|---|---|
/etc/pydhcp |
root:pydhcpd |
770 |
creates, renames and deletes entries | Others get nothing — no other account on the system can even enter the directory. Group w is the minimum that works: the daemon creates a temp file and os.replace()s it over the leases file, and creates and removes its PID file. 750 breaks all four operations. The group has exactly one member: the daemon's own service account (shell /bin/false, no home, no supplementary groups). No sticky bit — see the note below |
pydhcpd.py |
root:root |
755 |
reads and executes | Root-owned so the daemon cannot modify the code it is running. /etc/pydhcp being 770 already blocks every other account, so the others bits are what the daemon reads through |
pydhcpd.conf |
root:pydhcpd |
640 |
reads | Read through the group. Root-owned so a compromised daemon cannot rewrite its own configuration |
pydhcp.env |
root:pydhcpd |
640 |
reads | Same as above. 640 keeps it out of reach of other users |
pydhcpd.leases |
pydhcpd:pydhcpd |
640 |
replaces atomically | Daemon-owned because the daemon writes it: the atomic replace creates a new file and renames it over this one, so the result carries the writer's ownership |
pydhcpd.conf.bak |
root:root |
640 |
never touches it | Single rollback copy written by tools/pyleases.sh before it regenerates the config, and restored automatically if the daemon then fails to start. Created with cp, which preserves the source's 640 |
pydhcpd.conf.webmin.bak |
root:root |
640 |
never touches it | Single rollback copy written by the Webmin module (tools/pywebmin.sh) before each save from the browser editor. Kept under its own name, not pydhcpd.conf.bak, so the two never overwrite each other: that one belongs to pyleases.sh/uhmleases.sh and undoes an automatic rebuild, this one undoes a manual edit. Perl's File::Copy does not preserve the source mode, so config.cgi applies chmod 0640 explicitly — otherwise the backup would land at whatever root's umask dictates and end up more permissive than the config it copies |
/var/log/pydhcp.log |
pydhcpd:pydhcpd |
640 |
appends | Daemon-owned so it can write. pysetup.sh and tools/pyleases.sh also write to it, but they run as root |
tools/ |
root:root |
755 |
never touches it | Run manually by root; not part of the daemon's runtime |
pydhcpd.service |
root:root |
644 |
— | Belongs to systemd |
/etc/init.d/pydhcpd |
root:root |
755 |
— | Belongs to sysvinit |
/etc/logrotate.d/pydhcp |
root:root |
644 |
— | logrotate ignores configuration files not owned by root |
⚠️ WARNING: Do not "unify" these into a single owner. Making everythingpydhcpd:pydhcpdwould hand the directory to the daemon, which could then replace any entry in it, including its own code. Making everythingroot:pydhcpdwould break the leases and PID files, whose atomic replace and removal require ownership, and would needCAP_FOWNERto work around.
⚠️ WARNING: No "unifique" esto en un solo propietario. Poner todo enpydhcpd:pydhcpdle entregaría el directorio al demonio, que podría entonces reemplazar cualquier entrada, incluido su propio código. Poner todo enroot:pydhcpdrompería los archivos de concesiones y PID, cuyo reemplazo atómico y borrado exigen ser propietario, y requeriríaCAP_FOWNERpara sortearlo.
⚠️ WARNING: Why/etc/pydhcpcarries no sticky bit. A sticky bit (1770) would stop the daemon from deleting entries it does not own, which looks like an obvious hardening. It is not usable here: withfs.protected_regular=2— the default on several distributions — the kernel refuses to let any process, including root, truncate or replace a file owned by another user inside a sticky directory. That check ignores capabilities, soCAP_DAC_OVERRIDEdoes not bypass it. The lease-manager tools run as root and rewritepydhcpd.leases, which is owned by the daemon: with the sticky bit set they fail withEACCESand the reload chain aborts. The directory is therefore770, matching how this project's other service directories are set up.
⚠️ WARNING: Por qué/etc/pydhcpno lleva bit sticky. Un bit sticky (1770) impediría que el demonio borrara entradas que no le pertenecen, y parece un endurecimiento evidente. Aquí no es utilizable: confs.protected_regular=2— el valor por defecto en varias distribuciones — el núcleo impide que cualquier proceso, incluido root, trunque o reemplace un archivo de otro usuario dentro de un directorio con sticky. Esa comprobación ignora las capacidades, así queCAP_DAC_OVERRIDEno la sortea. Las herramientas de gestión de concesiones corren como root y reescribenpydhcpd.leases, que pertenece al demonio: con el sticky puesto fallan conEACCESy la cadena de recarga se aborta. Por eso el directorio es770, en línea con los demás directorios de servicio de este proyecto.
# Verify ownership and permissions | Verificar propietarios y permisos
sudo ls -ld /etc/pydhcp
sudo ls -l /etc/pydhcp/ /var/log/pydhcp.log| Topic | Description | Descripción |
|---|---|---|
| Entry points | pydhcpd can be managed through three entry points — systemctl, the /etc/init.d/pydhcpd wrapper, and pyleases.sh (which calls systemctl stop/start internally whenever it regenerates pydhcpd.conf). On a systemd host (the only supported environment — see Requirements), the init.d wrapper does not start its own process: it detects systemd and simply runs the equivalent systemctl command, so it and systemctl are always in sync, never two independent daemons. The only theoretical race is at the PID-file level (write_pid()) if the daemon were ever launched completely outside of systemd's management — not a realistic path on the supported environment, but as a matter of operational hygiene: use a single entry point at a time. Don't run pyleases.sh, systemctl, and the init.d wrapper concurrently against the same instance (e.g. don't kick off pyleases.sh in one terminal while manually restarting via systemctl in another) — let one lifecycle operation finish before starting the next. |
pydhcpd se puede administrar desde tres puntos de entrada — systemctl, el wrapper /etc/init.d/pydhcpd, y pyleases.sh (que llama internamente a systemctl stop/start cada vez que regenera pydhcpd.conf). En un host con systemd (el único entorno soportado — ver Requirements), el wrapper init.d no arranca su propio proceso: detecta systemd y simplemente ejecuta el systemctl equivalente, así que él y systemctl siempre están sincronizados, nunca son dos demonios independientes. La única carrera teórica ocurre a nivel del archivo PID (write_pid()) si el demonio se lanzara completamente por fuera de la gestión de systemd — no es un camino real en el entorno soportado, pero como buena práctica operativa: usa un solo punto de entrada a la vez. No corras pyleases.sh, systemctl y el wrapper init.d de forma concurrente sobre la misma instancia (p.ej. no lances pyleases.sh en una terminal mientras reiniciás manualmente con systemctl en otra) — dejá que termine una operación del ciclo de vida antes de iniciar la siguiente. |
| Automatic restart on failure | if pydhcpd crashes or exits with an error (e.g. the configured network interface is not present at startup), systemd restarts it automatically — pydhcpd.service sets Restart=on-failure with RestartSec=5 (retry every 5 seconds), capped at StartLimitBurst=10 attempts within a StartLimitIntervalSec=120 (2 minute) window. If the underlying problem is not resolved within those 10 attempts, systemd gives up and leaves the service in a failed state — it will not keep retrying indefinitely, and pydhcpd has no separate alerting mechanism to notify you when this happens. Check with systemctl status pydhcpd (a failed state needs a manual systemctl reset-failed pydhcpd before it can be started again) and watch /var/log/pydhcp.log / journalctl -u pydhcpd for the root cause. |
si pydhcpd falla o termina con un error (p.ej. la interfaz de red configurada no existe todavía al arrancar), systemd lo reinicia automáticamente — pydhcpd.service define Restart=on-failure con RestartSec=5 (reintenta cada 5 segundos), con un tope de StartLimitBurst=10 intentos dentro de una ventana de StartLimitIntervalSec=120 (2 minutos). Si el problema de fondo no se resuelve dentro de esos 10 intentos, systemd se da por vencido y deja el servicio en estado failed — no va a seguir reintentando indefinidamente, y pydhcpd no tiene un mecanismo de aviso separado que notifique cuando esto pasa. Verifica con systemctl status pydhcpd (un estado failed necesita un systemctl reset-failed pydhcpd manual antes de poder arrancarlo de nuevo) y revisa /var/log/pydhcp.log / journalctl -u pydhcpd para encontrar la causa raíz. |
ping-check |
ping-check true is enabled in the shipped pydhcpd.conf. The daemon sends a ping before each OFFER to verify the IP is not already in use, except in three cases: the client's MAC is a static host (host { fixed-address ... }), the offered IP is already the one that MAC currently holds, or the concurrent ping-check backlog (capped at 64 in-flight) is saturated — in the last case it fails open and sends the OFFER without checking, rather than blocking or dropping the DISCOVER. Waits up to ping-timeout seconds (default 1, read from pydhcpd.conf like ping-check itself) for a reply, but never blocks the main pool: each check runs on its own worker (capped at 4 concurrent) instead of stalling the DISCOVER handler like the standard blocking implementation. Internally, this ping is sent as a raw ICMP echo request built and read directly by the daemon (requires the CAP_NET_RAW capability, already granted in pydhcpd.service). If the raw socket cannot be opened (CAP_NET_RAW missing or revoked), it falls back to shelling out to the system's ping binary instead, logging an INFO when this happens (not a bad config value, so not a WARNING). A successful or failed result is cached for PING_CACHE_TTL_SECONDS seconds (default 120, read directly from pydhcp.env, see pydhcp.env — pydhcp-only extras) to avoid re-pinging the same IP on every DISCOVER during a burst. In environments with strict firewall rules blocking ICMP (on this host, the target, or in between), the request or its reply is silently dropped, the check times out the same way regardless of the underlying mechanism, and ping-check will have no effect. To disable it, set ping-check false; in /etc/pydhcp/core/pydhcpd.conf. If using pyleases.sh, set PING_CHECK_ENABLED=false in pydhcp.env instead — the script regenerates pydhcpd.conf on every run. |
ping-check true viene activado en el pydhcpd.conf enviado. El demonio envía un ping antes de cada OFFER para verificar que la IP no está en uso, excepto en tres casos: la MAC del cliente es un host estático (host { fixed-address ... }), la IP ofrecida ya es la que esa MAC tiene actualmente, o el cupo de verificaciones concurrentes (limitado a 64 en simultáneo) está saturado — en este último caso falla abierto y envía el OFFER sin verificar, en vez de bloquear o descartar el DISCOVER. Espera hasta ping-timeout segundos (default 1, leída de pydhcpd.conf igual que ping-check) por una respuesta, pero nunca bloquea el pool principal: cada verificación corre en su propio worker (limitado a 4 concurrentes) en vez de detener el manejo de DISCOVER como hace la implementación bloqueante estándar. Internamente, este ping se envía como una solicitud ICMP echo cruda, construida y leída directamente por el demonio (requiere el privilegio CAP_NET_RAW, ya concedido en pydhcpd.service). Si el socket crudo no se puede abrir (falta o se revoca CAP_NET_RAW), cae al binario ping del sistema como respaldo, registrando un INFO cuando esto ocurre (no es un valor malo de configuración, así que no es WARNING). Un resultado exitoso o fallido se cachea por PING_CACHE_TTL_SECONDS segundos (default 120, leída directamente de pydhcp.env, ver pydhcp.env — pydhcp-only extras) para evitar re-pingear la misma IP en cada DISCOVER durante una ráfaga. En entornos con reglas de firewall estrictas que bloquean ICMP (en este host, en el destino, o en el medio), la solicitud o su respuesta se descartan en silencio, la verificación expira igual sin importar el mecanismo interno, y ping-check no tendrá ningún efecto. Para desactivarlo, establece ping-check false; en /etc/pydhcp/core/pydhcpd.conf. Si usas pyleases.sh, establece PING_CHECK_ENABLED=false en pydhcp.env — el script regenera pydhcpd.conf en cada ejecución. |
| BPF filter on the raw socket | at startup, the daemon tries to attach an in-kernel BPF filter to its raw socket so only UDP/dst-port-67 frames wake the process — everything else is dropped by the kernel before reaching userspace. This is best-effort: pydhcpd runs with CAP_NET_RAW/CAP_NET_BIND_SERVICE only (see File Ownership and Permissions), and some kernels/containers restrict SO_ATTACH_FILTER to processes with CAP_NET_ADMIN/CAP_BPF instead. If the attach fails, the daemon logs an INFO and keeps running unaffected: the receive loop already filters every frame in userspace (ethertype, protocol, destination port) regardless of whether the kernel-level filter is active, so no DHCP functionality is lost — only non-DHCP traffic on the interface now also reaches the process before being discarded, instead of being dropped earlier by the kernel. Not a bad config value, so it is not a WARNING: it is an environment limitation the administrator cannot fix by editing a value. Example: 2026-08-25 10:15:32,123 INFO: BPF attach failed -- degraded |
al arrancar, el demonio intenta adjuntar un filtro BPF a nivel de kernel a su socket crudo para que solo los frames UDP con puerto destino 67 despierten al proceso — todo lo demás lo descarta el kernel antes de llegar a userspace. Esto es un intento sin garantía: pydhcpd corre solo con CAP_NET_RAW/CAP_NET_BIND_SERVICE (ver File Ownership and Permissions), y algunos kernels/contenedores restringen SO_ATTACH_FILTER a procesos con CAP_NET_ADMIN/CAP_BPF. Si el intento falla, el demonio registra un INFO y sigue funcionando sin verse afectado: el bucle de recepción ya filtra cada frame en userspace (ethertype, protocolo, puerto destino) sin importar si el filtro a nivel de kernel está activo, así que no se pierde ninguna funcionalidad DHCP — solo que el tráfico no-DHCP de la interfaz también llega ahora al proceso antes de descartarse, en vez de ser descartado antes por el kernel. No es un valor malo de configuración, así que no es WARNING: es una limitación del entorno que el administrador no puede corregir editando un valor. Ejemplo: 2026-08-25 10:15:32,123 INFO: BPF attach failed -- degraded |
cleanup-interval |
cleanup-interval controls how often (in seconds) the daemon removes expired leases from memory. The default is 60. If you use a short pool lease-time (e.g. 10 or 30 seconds), set cleanup-interval to the same value or lower so that expired leases are freed promptly and the pool does not appear exhausted. When using pyleases.sh, set CLEANUP_INTERVAL in pydhcp.env — it is written into pydhcpd.conf on every run. Config validation logs a WARNING (not an error — the daemon still starts) if cleanup-interval is greater than the pool's min-lease-time. Minimum enforced value: 5 seconds — if you set a lower value, the daemon clamps it to 5 and logs a WARNING stating the requested value. |
cleanup-interval controla con qué frecuencia (en segundos) el demonio elimina los arrendamientos expirados de la memoria. El valor por defecto es 60. Si usas un lease-time corto en el pool (p.ej. 10 o 30 segundos), establece cleanup-interval al mismo valor o menor para que los arrendamientos expirados se liberen rápidamente y el pool no parezca agotado. Al usar pyleases.sh, define CLEANUP_INTERVAL en pydhcp.env — se escribe en pydhcpd.conf en cada ejecución. La validación de configuración registra un WARNING (no un error — el demonio igual arranca) si cleanup-interval es mayor que el min-lease-time del pool. Valor mínimo forzado: 5 segundos — si se establece un valor menor, el demonio lo recorta a 5 y registra un WARNING indicando el valor solicitado. |
| Pool lease time default | the block pool's min-lease-time / default-lease-time / max-lease-time default to 60 seconds, consistently across every path in this project: the shipped pydhcpd.conf template ships with 60 written explicitly in the pool { } block, pyleases.sh writes 60 (its CLEANUP_INTERVAL default) into the pool block on a fresh install, and pydhcpd.py's own built-in fallback is also 60 (used only if a hand-written config omits the pool lease-time lines entirely). This keeps the default consistent with the short-lived, temporary nature of the block pool — unknown clients get a brief lease that is quickly recycled, unlike AUTHORIZED_LEASE_TIME (default 2592000s / 30 days) used for the subnet-level lease given to authorized/static clients.To change it: this is a per-installation choice, not something you edit in the project's code. At install time, pysetup.sh writes your answer to the CLEANUP_INTERVAL prompt directly into the pool { } block of pydhcpd.conf. To change it afterwards: if you manage pydhcpd.conf by hand, edit the pool { min-lease-time / default-lease-time / max-lease-time } values directly in your live /etc/pydhcp/core/pydhcpd.conf and restart/reload the daemon. If you use pyleases.sh, edit CLEANUP_INTERVAL in your /etc/pydhcp/pydhcp.env and re-run pyleases.sh — it rewrites pydhcpd.conf from that value on every run. |
el min-lease-time / default-lease-time / max-lease-time del pool de bloqueo tienen por defecto 60 segundos, de forma consistente en los tres caminos del proyecto: la plantilla pydhcpd.conf incluida trae 60 escrito explícitamente en el bloque pool { }, pyleases.sh escribe 60 (su valor por defecto de CLEANUP_INTERVAL) en el bloque del pool en una instalación nueva, y el respaldo interno propio de pydhcpd.py también es 60 (se usa solo si una configuración escrita a mano omite por completo las líneas de lease-time del pool). Esto mantiene el valor por defecto consistente con la naturaleza breve y temporal del pool de bloqueo — los clientes desconocidos reciben un lease corto que se recicla rápido, a diferencia de AUTHORIZED_LEASE_TIME (por defecto 2592000s / 30 días) usado para el lease a nivel de subred que reciben los clientes autorizados/estáticos.Para cambiarlo: es una decisión de cada instalación, no algo que se edite en el código del proyecto. Al instalar, pysetup.sh escribe tu respuesta a la pregunta CLEANUP_INTERVAL directamente en el bloque pool { } de pydhcpd.conf. Para cambiarlo después: si administras pydhcpd.conf a mano, edita los valores de pool { min-lease-time / default-lease-time / max-lease-time } directamente en tu /etc/pydhcp/core/pydhcpd.conf real y reinicia/recarga el demonio. Si usas pyleases.sh, edita CLEANUP_INTERVAL en tu /etc/pydhcp/pydhcp.env y vuelve a correr pyleases.sh — reescribe pydhcpd.conf a partir de ese valor en cada ejecución. |
| IP quarantine | when an IP is quarantined — either because a client sent a DHCPDECLINE (ignored by default, see deny declines; above) or because ping-check detects it is already in use before an OFFER — it is held out of the pool for abandon-lease-time seconds, 60 by default. Read from pydhcpd.conf, like every other behavior directive — not from pydhcp.env. If using pyleases.sh, set QUARANTINE_DURATION in pydhcp.env instead — the script writes it into pydhcpd.conf as abandon-lease-time on every run, same as CLEANUP_INTERVAL/ping-check. Picked up live on SIGHUP/reload, no restart needed. It is independent from the pool's default-lease-time (see below); the two are not required to match. |
cuando una IP se pone en cuarentena — ya sea porque un cliente envió un DHCPDECLINE (ignorado por defecto, ver deny declines; arriba) o porque ping-check detecta que ya está en uso antes de un OFFER — se aparta del pool por abandon-lease-time segundos 60 por defecto. Se lee de pydhcpd.conf, igual que cualquier otra directiva de comportamiento — no de pydhcp.env. Si usas pyleases.sh, define QUARANTINE_DURATION en pydhcp.env — el script la escribe en pydhcpd.conf como abandon-lease-time en cada ejecución, igual que CLEANUP_INTERVAL/ping-check. Se aplica en caliente con SIGHUP/reload, sin reiniciar. Es independiente del default-lease-time del pool (ver más abajo); no es necesario que coincidan. |
| Pool range cap | the pool { range A B; } directive is capped at 65536 addresses (a /16). The daemon builds the full address set in memory at startup and re-sorts the free set on every allocation, so an oversized range (e.g. a /8) would waste memory and CPU proportional to its size. A range larger than the cap is rejected at config load (or SIGHUP reload) with a clear error instead of being silently accepted. |
la directiva pool { range A B; } tiene un tope de 65536 direcciones (un /16). El demonio construye el conjunto completo de direcciones en memoria al arrancar y reordena el conjunto libre en cada asignación, por lo que un rango sobredimensionado (p.ej. un /8) desperdiciaría memoria y CPU proporcional a su tamaño. Un rango mayor al tope se rechaza al cargar la configuración (o al recargar con SIGHUP) con un error claro, en vez de aceptarse en silencio. |
pyleases.sh — Advanced DHCP lease and ACL manager for pydhcpd. Parses pydhcpd.leases, detects unauthorized clients, rebuilds pydhcpd.conf from ACL files, and restarts the daemon. Designed for environments enforcing DHCP-based access control.ACL directories: /etc/acl/mac/ (administrator's own, authorized: mac-limited.txt, mac-unlimited.txt) and /etc/pydhcp/acl/ (pydhcp's own, blocked: blockdhcp.txt).Entry format: a;MAC;IP;HOSTNAME;. The leading a means "active" and is what marks a well-formed entry — any other leading character is malformed (see ACL priority order). There is no opposite value: for the mac-*.txt lists, to deactivate an entry, comment out the whole line by prefixing it with # (#a;MAC;IP;HOSTNAME;) instead of editing the a itself. blockdhcp.txt is the exception: it has no active/inactive state and no # syntax — an entry's mere presence blocks the MAC. To unblock, delete the line; a #-prefixed line there is dropped as malformed.A duplicate MAC, IP or hostname is compared on the value alone — a commented ( #a;) line counts the same as an active one, since deactivating an entry does not remove it from the file. What happens with each list is in ACL priority order.When pyleases.sh blocks a client it also removes it from pydhcpd.leases, so the IP it was using is free for another client at that same instant: a MAC in blockdhcp.txt never holds a lease.An IP in mac-*.txt that falls inside the blockdhcp pool range is a misconfiguration: pyleases.sh aborts the run before the daemon is stopped or pydhcpd.conf is rewritten, naming the MAC to move. Commented-out lines are not checked.
|
pyleases.sh — Gestor avanzado de concesiones y ACLs DHCP para pydhcpd. Parsea pydhcpd.leases, detecta clientes no autorizados, reconstruye pydhcpd.conf a partir de archivos ACL y reinicia el demonio. Diseñado para entornos que aplican control de acceso basado en DHCP.Directorios ACL: /etc/acl/mac/ (propios del administrador, autorizados: mac-limited.txt, mac-unlimited.txt) y /etc/pydhcp/acl/ (propio de pydhcp, bloqueados: blockdhcp.txt).Formato: a;MAC;IP;HOSTNAME;. La a inicial significa "active" (activo) y es lo que marca una entrada bien formada — cualquier otro carácter inicial es malformado (ver ACL priority order). No existe un valor opuesto: para las listas mac-*.txt, para desactivar una entrada se comenta la línea completa agregando # al inicio (#a;MAC;IP;HOSTNAME;) en vez de editar la a misma. blockdhcp.txt es la excepción: no tiene estado activo/inactivo ni sintaxis # — la sola presencia de una entrada bloquea la MAC. Para desbloquear, se borra la línea; una línea con # ahí se elimina por malformada.Una MAC, IP u hostname duplicado se compara solo por el valor — una línea comentada ( #a;) cuenta igual que una activa, ya que desactivar una entrada no la quita del archivo. Qué pasa con cada lista está en ACL priority order.Cuando pyleases.sh bloquea a un cliente, además lo elimina de pydhcpd.leases, de modo que la IP que estaba usando queda libre para otro cliente en ese mismo instante: una MAC de blockdhcp.txt nunca tiene un lease.Una IP de mac-*.txt que caiga dentro del rango del pool blockdhcp es un error de configuración: pyleases.sh aborta la corrida antes de detener el demonio y antes de reescribir pydhcpd.conf, nombrando la MAC que hay que mover. Las líneas comentadas no se revisan.
|
sudo bash tools/pyleases.sh| ACL | Priority Level | Description | Descripción |
|---|---|---|---|
mac-unlimited.txt |
1 | List maintained by hand by the administrator. Designed for communications hardware, servers and other essential equipment, not subject to firewall restrictions. A malformed line aborts with ERROR. |
Lista mantenida manualmente por el administrador. Está diseñada para hardware de comunicaciones, servidores y otros equipos esenciales, no sujetos a restricciones del firewall. Una línea malformada aborta con ERROR. |
mac-limited.txt |
2 | List maintained by hand by the administrator. Designed for equipment joining the local network. May be subject to firewall, proxy and other restrictions. A malformed line aborts with ERROR. |
Lista mantenida manualmente por el administrador. Está diseñada para los equipos que se integran a una red local. Puede estar sujeta a restricciones de firewall, proxy, etc. Una línea malformada aborta con ERROR. |
blockdhcp.txt |
0 | List operated by the pydhcp daemon and written by pyleases.sh. Designed for clients denied a DHCP lease outright: any client with no entry in mac-*.txt is added here and loses its lease at that same instant. Authorizes nothing on its own. A malformed line is dropped with INFO and the run continues. |
Lista operada por el demonio pydhcp y escrita por pyleases.sh. Está diseñada para los clientes a los que se les niega el lease DHCP por completo: todo cliente sin entrada en mac-*.txt se agrega aquí y pierde su lease en ese mismo instante. No autoriza nada por sí sola. Una línea malformada se elimina con INFO y la corrida continúa. |
Lines starting with
#are treated as deactivated and get blocked. Only applies to the ACLs with Priority Level 1 and 2.Las líneas que comienzan con
#se consideran desactivadas y serán bloqueadas. Solo aplica a las ACL con Priority Level 1 y 2.
Warning
|
|
pyleases.sh generates /etc/pydhcp/core/pydhcpd.conf dynamically on every run. WPAD/PAC support is controlled entirely from pydhcp.env — no manual editing of pyleases.sh is required.
|
pyleases.sh genera /etc/pydhcp/core/pydhcpd.conf dinámicamente en cada ejecución. El soporte WPAD/PAC se controla completamente desde pydhcp.env — no se requiere editar manualmente pyleases.sh.
|
To enable/disable WPAD:
WPAD_ENABLED=true:
pyleases.sh never trusts WPAD_ENABLED=true on its own. On every run it fetches http://SERVER_IP:WPAD_PORT/wpad.pac and only writes the two option wpad lines if it gets an HTTP 200. Otherwise it logs a WARNING, leaves the lines commented out, and continues normally. This prevents the failure mode where every WPAD-aware client on the LAN stalls on an unreachable PAC URL — a fault that produces no error on the server and shows up only as "the network is slow" everywhere at once. Verify it yourself with:
curl -fsS --noproxy '*' --max-time 5 -o /dev/null "http://SERVER_IP:WPAD_PORT/wpad.pac"; echo $?A result of 0 means WPAD will be activated; anything else means it will not.
The project does not deploy the Apache side: no VirtualHost, no wpad.pac. That setup is the administrator's responsibility.
|
Para activar/desactivar WPAD:
WPAD_ENABLED=true:
pyleases.sh nunca confía en WPAD_ENABLED=true por sí solo. En cada ejecución descarga http://SERVER_IP:WPAD_PORT/wpad.pac y solo escribe las dos líneas option wpad si obtiene un HTTP 200. Si no, registra un WARNING, deja las líneas comentadas y continúa con normalidad. Esto evita el fallo en que todos los clientes de la red que atienden WPAD se quedan esperando una URL PAC inalcanzable — una avería que no produce ningún error en el servidor y que solo se manifiesta como "la red está lenta" en todas partes a la vez. Verifíquelo usted mismo con:
curl -fsS --noproxy '*' --max-time 5 -o /dev/null "http://SERVER_IP:WPAD_PORT/wpad.pac"; echo $?Un resultado 0 significa que WPAD se activará; cualquier otro, que no.
El proyecto no despliega la parte de Apache: ni el VirtualHost ni el wpad.pac. Ese montaje es responsabilidad del administrador.
|
Android and iOS ignore DHCP option 252. The proxy must be configured manually on those devices.
Android e iOS ignoran la opción DHCP 252. El proxy debe configurarse manualmente en esos dispositivos.
| Command | Description | Descripción |
|---|---|---|
sudo bash bkstack.sh |
Create a backup now | Crear una copia ahora |
sudo bash bkstack.sh install |
Register the @monthly cron entry |
Registrar la entrada mensual en cron |
sudo bash bkstack.sh uninstall |
Remove the cron entry, keeping the archives | Quitar la entrada de cron, conservando los comprimidos |
Backs up both projects into
/etc/bak/pydhcp/bkstack_<TIMESTAMP>.zip: their install trees, the shared ACL lists, the systemd units, theinit.dwrapper, the logrotate config and the Webmin modules. Paths that do not exist are skipped, so it works whetheruhmis installed or onlypydhcp. The archive lives outside/etc/pydhcpand/etc/uhm, so uninstalling either project never touches it. Restore by unzipping it over/.Respalda ambos proyectos en
/etc/bak/pydhcp/bkstack_<TIMESTAMP>.zip: sus árboles de instalación, las listas ACL compartidas, las unidades de systemd, el wrapper deinit.d, la configuración de logrotate y los módulos de Webmin. Las rutas que no existan se omiten, así que funciona tanto conuhminstalado como solo conpydhcp. El comprimido vive fuera de/etc/pydhcpy/etc/uhm, así que desinstalar cualquiera de los dos no lo toca. Para restaurar, descomprímalo sobre/.
There are two kinds of backup in this project and they follow different rules. A project backup is a copy of the whole install, kept for the administrator: it goes to /etc/bak/pydhcp, carries a timestamp and keeps up to 3 copies. Only bkstack.sh writes one. A routine-operation backup is the copy a script takes of one specific file right before modifying it, so the change can be undone: it goes next to the file it copies, as <file>.bak, and keeps a single copy overwritten on every run. What decides the kind is what is copied, not how long the copy lasts.
|
En este proyecto hay dos clases de respaldo y no se rigen igual. Un respaldo de proyecto es la copia de la instalación entera, guardada para el administrador: va a /etc/bak/pydhcp, lleva marca de tiempo y conserva hasta 3 copias. Solo bkstack.sh escribe una. Un respaldo de operación rutinaria es la copia que un script toma de un archivo concreto justo antes de modificarlo, para poder deshacer el cambio: va junto al archivo que copia, como <archivo>.bak, y conserva una sola copia, sobrescrita en cada ejecución. Lo que decide la clase es qué se copia, no cuánto dura la copia.
|
| pywebmin.sh — Optional installer for a PyDHCP module for Webmin. Provides a web interface to manage the pydhcpd daemon: service control (start/stop/restart/reload), active leases table, and configuration file editor. Requires Webmin to be installed on the system. | pywebmin.sh — Instalador opcional de un módulo PyDHCP para Webmin. Proporciona una interfaz web para administrar el demonio pydhcpd: control del servicio (start/stop/restart/reload), tabla de concesiones activas y editor del archivo de configuración. Requiere que Webmin esté instalado en el sistema. |
| Feature | Description | Descripción |
|---|---|---|
| Service control | Start / Stop / Restart / Reload buttons for pydhcpd. |
Botones Iniciar / Detener / Reiniciar / Recargar para pydhcpd. |
| Active leases table | IP, MAC, hostname, expiry and binding state, read from pydhcpd.leases. |
IP, MAC, hostname, expiración y estado, leídos de pydhcpd.leases. |
| Config editor | Edits pydhcpd.conf in the browser; validated with pydhcpd.py -t -cf before saving, rejected on syntax error. Before each save it keeps a single rollback copy as pydhcpd.conf.webmin.bak, next to the file. |
Edita pydhcpd.conf en el navegador; se valida con pydhcpd.py -t -cf antes de guardar, se rechaza si hay error de sintaxis. Antes de cada guardado conserva una única copia de rollback como pydhcpd.conf.webmin.bak, junto al archivo. |
| State badges | Color-coded: Active (#d4edda/#155724), Inactive (#f8d7da/#721c24), Unknown (#e2e3e5/#383d41), Warning (#fff3cd/#856404). |
Con color: Active (#d4edda/#155724), Inactive (#f8d7da/#721c24), Unknown (#e2e3e5/#383d41), Warning (#fff3cd/#856404). |
# Install | Instalar
sudo bash tools/pywebmin.sh install
# Uninstall | Desinstalar
sudo bash tools/pywebmin.sh uninstallAccess is granted to the Webmin
rootaccount and to the detected local sudo user. For any other Webmin user, grant it from Webmin → Webmin Users.El acceso se concede a la cuenta
rootde Webmin y al usuario local con sudo detectado. Para cualquier otro usuario de Webmin, concederlo desde Webmin → Webmin Users.
| Aspect | authoritative |
DHCP Snooping | Description | Descripción |
|---|---|---|---|---|
| Where it acts | the DHCP server | the switch | One is a daemon directive, the other a network-layer feature | Uno es una directiva del demonio, el otro una función de la red |
| When | after the fact, on the REQUEST | before, on the traffic itself | Correction versus prevention | Corrección frente a prevención |
| What it does | NAKs the request, forcing the client to rediscover | drops the rogue's packets | The client ends up with the correct IP either way | El cliente termina con la IP correcta en ambos casos |
| Reach | cannot stop DHCPOFFER/DHCPACK from reaching the client |
the client never receives them | No DHCP server can block another's packets at the protocol level | Ningún servidor DHCP puede bloquear los paquetes de otro a nivel de protocolo |
A rogue server may win the OFFER race, but the client's REQUEST is broadcast and always reaches the authoritative server, which NAKs it and forces the client to discard the rogue lease and start over. Check whether your switch supports DHCP Snooping and enable it if so: it blocks rogue DHCP traffic before it ever reaches a client, instead of correcting it afterwards.
Un servidor no autorizado puede ganar la carrera del OFFER, pero el REQUEST del cliente se manda por broadcast y siempre llega al servidor autoritativo, que lo rechaza con NAK y fuerza al cliente a descartar esa concesión y empezar de nuevo. Verifique si su switch soporta DHCP Snooping y actívelo si es así: bloquea el tráfico DHCP no autorizado antes de que llegue al cliente, en vez de corregirlo después.
| Add the following rules to allow DHCP traffic on both interfaces. The WAN rules cover the case where the server itself acts as a DHCP client toward an upstream router. The LAN rules allow the server to assign IP addresses to local clients. | Agregue las siguientes reglas para permitir el tráfico DHCP en ambas interfaces. Las reglas de WAN cubren el caso en el que el propio servidor actúa como cliente DHCP hacia un enrutador ascendente. Las reglas de LAN permiten al servidor asignar direcciones IP a clientes locales. |
# WAN — DHCP client (server requests an IP from an upstream DHCP server)
iptables -A OUTPUT -o $wan -p udp --sport 68 --dport 67 -j ACCEPT
iptables -A INPUT -i $wan -p udp --sport 67 --dport 68 -j ACCEPT
# LAN — DHCP server (server assigns IPs to local clients)
iptables -A INPUT -i $lan -p udp --sport 68 --dport 67 -j ACCEPT
iptables -A OUTPUT -o $lan -p udp --sport 67 --dport 68 -j ACCEPT
pydhcpd writes logs directly to /var/log/pydhcp.log. It does not use syslog, therefore no log-facility directive is needed or supported. That single file is shared by the whole project — the daemon, pysetup.sh and tools/pyleases.sh all append to it, under one daily rotation (/etc/logrotate.d/pydhcp). Its path is fixed and not configurable.
|
pydhcpd escribe los logs directamente a /var/log/pydhcp.log. No utiliza syslog, por lo tanto no se necesita ni se soporta la directiva log-facility. Ese archivo único lo comparte todo el proyecto — el demonio, pysetup.sh y tools/pyleases.sh escriben en él, bajo una sola rotación daily (/etc/logrotate.d/pydhcp). Su ruta es fija y no configurable.
|
| Level | Description | Descripción |
|---|---|---|
ERROR: |
Exclusively for a message that aborts the current flow -- the process/script stops right there, nothing after it runs. Always paired with the -- abort suffix. |
Exclusivo para un mensaje que aborta el flujo actual -- el proceso/script se detiene ahí mismo, nada después corre. Siempre acompañado del sufijo -- abort. |
WARNING: |
Something is seriously wrong and needs the administrator's immediate attention, but execution does not abort. Paired with -- alert (a live condition needing supervision, e.g. a possible attack or resource saturation) or -- fallback (the administrator supplied a bad/out-of-range value in the config, and the daemon used a built-in default instead -- the value must be corrected). |
Algo anda mal y requiere atención inmediata del administrador, pero la ejecución no aborta. Acompañado de -- alert (una condición en vivo que amerita supervisión, ej. un posible ataque o saturación de recursos) o -- fallback (el administrador puso un valor malo o fuera de rango en la configuración, y el demonio usó un valor por defecto en su lugar -- ese valor debe corregirse). |
INFO: |
Everything else: routine state changes, notifications, and anything skipped or defaulted without needing administrator attention. Paired with -- skip (an action or packet was discarded, for any reason) or -- degraded (a system/environment limitation -- not a bad config value -- left the daemon running without an optimization or protection it would normally have; nothing for the administrator to fix). |
Todo lo demás: cambios de estado rutinarios, notificaciones, y cualquier cosa omitida o resuelta con un valor por defecto sin necesitar atención del administrador. Acompañado de -- skip (se descartó una acción o paquete, por cualquier razón) o -- degraded (una limitación del sistema/entorno -- no un valor malo de configuración -- dejó al demonio funcionando sin una optimización o protección que normalmente tendría; no hay nada que el administrador deba corregir). |
No log line exceeds 80 columns, timestamp and level included. Long values are cut at write time so a line can never wrap. MAC and IP addresses are never affected — their limits are 17 and 15 characters, which is exactly what they measure. Everything else is capped:
grep by level would miss.
|
Ninguna línea de log supera las 80 columnas, contando fecha y nivel. Los valores largos se cortan al escribirse para que una línea nunca se parta sola. Las direcciones MAC e IP nunca se ven afectadas — sus topes son 17 y 15 caracteres, justo lo que miden. Todo lo demás tiene tope:
grep por nivel no encontraría.
|
| Project | Version | EOL Date |
|---|---|---|
| ISC-DHCP | 4.4.3-P1-4ubuntu2 | 2022 |
- Archify
- Webmin (optional, required by
tools/pywebmin.sh) - Maintenance Scripts (pyleases, bkstack, pywebmin)
This repository
|
Este repositorio
|
| This project uses a dual-licensing model to balance software freedom with content protection: | Este proyecto utiliza un modelo de licencia dual para equilibrar la libertad del software con la protección del contenido: |
| Content | Licensed Under |
|---|---|
| Scripts, Binaries, Infrastructure | |
| RAG, Workers, Specialized Modules, Docs |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

