UHM — UniFi Hotspot Manager
|
Many businesses, institutions, and other environments use Ubiquiti UniFi networks with access points, switches, and gateways. UniFi provides tools to manage the network, authenticate users, and control access. In some environments, the limitation lies not in the hardware or its cost, but in the available level of control and customization. They may need more specific access rules, finer filtering, usage limits, or services that the gateway does not provide or provides only in a limited way. UHM runs UniFi Network self-hosted on Linux and configures the UniFi gateway in Third-Party Gateway mode. UniFi keeps its native network functions, while Linux applies policies and provides additional services. Users first authenticate through the UniFi captive portal with a voucher. After the voucher is redeemed, UHM applies the policies and controls configured in Linux to the connection. These functions include DHCP, MAC-to-IP mapping, access control lists (ACLs), firewall rules with Because it is built on Linux, UHM can integrate additional services as the environment requires, such as Unbound, Squid, Suricata, Samba, among others. UniFi provides the wireless network, captive portal, and voucher authentication. UHM adds Linux-based controls that let administrators tailor the network to their environment. If Proxymon is also integrated, you can monitor traffic and limit data usage on connections with a data allowance. UHM is not intended to replace UniFi. It extends its capabilities with a Linux platform for more detailed, customizable, and extensible network and security policies. |
Muchas empresas, instituciones y otros entornos usan redes Ubiquiti UniFi con puntos de acceso, switches y gateways. UniFi ofrece herramientas para administrar la red, autenticar usuarios y controlar el acceso. En algunos entornos, la limitación no está en el hardware ni en su costo, sino en el nivel de control y personalización disponible. Puede hacer falta definir reglas de acceso más específicas, filtrar con mayor detalle, limitar el consumo o incorporar servicios que el gateway no ofrece o que ofrece de forma limitada. UHM propone ejecutar UniFi Network self-hosted en Linux y configurar el gateway UniFi en modo Third-Party Gateway. UniFi conserva las funciones nativas de la red, mientras Linux aplica las políticas y presta servicios adicionales. La autenticación inicial se realiza en el portal cautivo de UniFi mediante vouchers. Después de canjear uno, UHM aplica a la conexión las políticas y los controles configurados en Linux. Entre esas funciones están DHCP, la asociación de direcciones MAC e IP, las listas de control de acceso (ACL), las reglas de firewall con Como se basa en Linux, UHM puede integrar servicios adicionales según las necesidades del entorno, como Unbound, Squid, Suricata, Samba, entre otros. UniFi mantiene la red inalámbrica, el portal cautivo y la autenticación mediante vouchers. UHM añade controles desde Linux para adaptar la red a las necesidades del entorno. Si integras Proxymon, también puedes supervisar el tráfico y limitar el consumo en conexiones con una cuota de datos. UHM no pretende sustituir a UniFi, sino ampliar sus capacidades con una plataforma Linux que permite aplicar políticas de red y seguridad más detalladas, personalizadas y extensibles. |
UniFi gateway alone:
| Stage | Description | Descripción |
|---|---|---|
| Joins the SSID | DHCP lease from the gateway | Concesión DHCP del gateway |
| Before redeeming a voucher | Held at the captive portal by the AP. Tracked only as an unauthorized guest session | Retenido en el portal cautivo por el AP. Solo se rastrea como sesión de invitado no autorizada |
| Redeems a valid voucher | Marked authorized; keeps whatever IP it already had | Queda autorizado; conserva la IP que ya tenía |
| While authorized | Full access until the voucher expires | Acceso completo hasta que expire el voucher |
| Voucher expires | Back to the captive portal; must redeem another one | Vuelve al portal cautivo; debe canjear otro |
| Never redeems a voucher | Remains at the portal and keeps a DHCP lease while retrying | Permanece en el portal y conserva una concesión DHCP mientras vuelve a intentarlo |
| Admin unauthorizes / deletes the voucher | Client returns to the portal | El cliente vuelve al portal |
| Corporate / infrastructure devices | Need a separate SSID, VLAN or manual per-client authorization | Requieren un SSID aparte, una VLAN o autorización manual por cliente |
| Durable record of voucher activity | stat/voucher drops a voucher once it expires or its quota runs out |
stat/voucher descarta un voucher cuando expira o se agota su cuota |
| Hardware required | UDM, UDM-Pro, Cloud Key or equivalent gateway | UDM, UDM-Pro, Cloud Key o gateway equivalente |
Unifi Hotspot Manager - UHM:
| Stage | Description | Descripción |
|---|---|---|
| Joins the SSID | DHCP lease from pydhcpd, assigned from the block pool range (SERV_INI_RANGE_BLOCK-SERV_END_RANGE_BLOCK) |
Concesión DHCP de pydhcpd, asignada desde el rango de bloqueo (SERV_INI_RANGE_BLOCK-SERV_END_RANGE_BLOCK) |
| Before redeeming a voucher | Added to uhm-grace.txt with the time of first contact. The macgrace ipset limits access to the portal ports and DNS to the configured resolvers |
Se añade a uhm-grace.txt con la hora del primer contacto. El ipset macgrace limita el acceso a los puertos del portal y al DNS de los resolvers configurados |
| Redeems a valid voucher | Added to uhm-auth.txt, assigned a fixed IP in the hotspot range, DHCP lease released, and disconnected so it reconnects with the new IP |
Se añade a uhm-auth.txt, recibe una IP fija del rango del hotspot, se libera su concesión DHCP y se desconecta al cliente para que vuelva a conectarse con la IP nueva |
| While authorized | Same, plus firewall enforcement via the machotspot ipset and optional Squid/proxy routing |
Igual, más la aplicación de firewall vía el ipset machotspot y el enrutamiento opcional por Squid/proxy |
| Voucher expires | Removed from uhm-auth.txt, lease released, re-enters uhm-grace.txt with a fresh grace timer — same as a brand-new client |
Se elimina de uhm-auth.txt, se libera su lease y vuelve a entrar a uhm-grace.txt con un temporizador de gracia nuevo — igual que un cliente recién llegado |
| Never redeems a voucher | After BLOCKDHCP_GRACE_SECONDS (default 24h) it moves permanently to blockdhcp.txt and pydhcpd stops assigning it an IP address |
Tras BLOCKDHCP_GRACE_SECONDS (default 24h) pasa permanentemente a blockdhcp.txt y pydhcpd deja de asignarle una dirección IP |
| Admin unauthorizes / deletes the voucher | Removed from uhm-auth.txt and sent back through the grace cycle. The stale UniFi session it leaves behind cannot re-authorize it — only a new voucher can |
Se elimina de uhm-auth.txt y vuelve al ciclo de gracia. La sesión residual que UniFi deja atrás no puede reautorizarlo: solo un voucher nuevo puede |
| Corporate / infrastructure devices | Listed in mac-*.txt: fixed address and no timer at the DHCP level, plus automatic authorize-guest in UniFi every cycle so the AP never holds them at the portal on a Guest/Hotspot LAN |
Se listan en mac-*.txt: dirección fija y sin temporizador a nivel DHCP, más authorize-guest automático en UniFi cada ciclo para que el AP nunca los retenga en el portal en una WLAN Guest/Hotspot |
| Durable record of voucher activity | /var/log/uhm.log keeps the full history, and uhmunifi.sh cross-references it against the live controller |
/var/log/uhm.log conserva el historial completo, y uhmunifi.sh lo cruza contra el controlador en vivo |
| Hardware required | One UniFi AP plus a Linux host running the self-hosted controller | Un AP UniFi más un host Linux corriendo el controlador self-hosted |
| Description | Descripción |
|---|---|
| Tested on Ubuntu 24.04/26.04 LTS. Use on other versions or distributions at your own risk. | Probado en Ubuntu 24.04/26.04 LTS. El uso en otras versiones o distribuciones queda bajo tu responsabilidad. |
Install UHM on a clean system. uhmsetup.sh installs pydhcp as the DHCP server and apache2 for the web panel and WPAD; it also configures the firewall. If you enable the panel or WPAD, the installer modifies /etc/apache2/ports.conf, creates a VirtualHost under sites-available/, and adds a rule under sudoers.d/. |
Se recomienda instalar UHM en un sistema limpio. uhmsetup.sh instala pydhcp como servidor DHCP y apache2 para el panel web y WPAD; también configura el firewall. Si activas el panel o WPAD, el instalador modifica /etc/apache2/ports.conf, crea un VirtualHost en sites-available/ y añade una regla en sudoers.d/. |
Before installing, the script checks for conflicting software. It stops if it finds another DHCP or web server (isc-dhcp-server, kea-dhcp4-server, udhcpd, nginx, lighttpd, or caddy), firewalld, or an active ufw. It only warns about dnsmasq. apache2 and squid are not conflicts: UHM installs apache2, and the reference firewall rules expect squid. |
Antes de instalar, el script busca programas que podrían entrar en conflicto. Si encuentra otro servidor DHCP o web (isc-dhcp-server, kea-dhcp4-server, udhcpd, nginx, lighttpd o caddy), firewalld o un ufw activo, detiene la instalación. Si encuentra dnsmasq, solo muestra una advertencia. apache2 y squid no se consideran conflictos: UHM instala el primero y las reglas de firewall de referencia contemplan el segundo. |
| Resource | Minimum |
|---|---|
| CPU | 2 cores |
| RAM | 4 GB |
| Disk | 8 GB |
These are approximate values. UniFi Network self-hosted accounts for most of the resource use, which varies by version, number of managed devices, and environment. UHM itself (
uhmd.sh,uhmleases.sh, andpydhcpd.py) adds little overhead.Son valores aproximados: UniFi Network self-hosted determina la mayor parte del consumo, que puede variar según la versión, la cantidad de dispositivos y el entorno. UHM (
uhmd.sh,uhmleases.shypydhcpd.py) añade una carga mínima.
| Component | Tested Version |
|---|---|
| UniFi OS Server | 5.1.15 |
| UniFi Network (self-hosted) | 10.4.57 |
iptables |
1.8.10 |
ipset |
7.19 |
pydhcpd |
latest |
UHMonly checks whether UniFi Network self-hosted or UniFi OS Server is installed; it does not install either one. If neither is installed, first useunifisetup.shto install the controller, then runuhmsetup.sh.
UHMsolo comprueba si UniFi Network self-hosted o UniFi OS Server está instalado; no instala ninguno. Si todavía no tienes uno, instala primero el controlador conunifisetup.shy luego ejecutauhmsetup.sh.
UHM is designed to manage one guest network. Each installation supports:
pydhcp and UHM manage only one Network and one IPv4 subnet. The UniFi controller may contain other Networks, VLANs, or ESSIDs. They can operate normally, but they are outside UHM's scope and need their own DHCP, routing, and firewall services. A controller can therefore host the Network managed by UHM alongside other Networks managed by separate infrastructure, but UHM manages only one Network. |
UHM está diseñado para administrar una sola red de invitados. Cada instalación admite:
pydhcp y UHM solo administran una Network y una subred IPv4. El controlador UniFi puede incluir otras Networks, VLAN o ESSID. Pueden funcionar con normalidad, pero quedan fuera del alcance de UHM y necesitan sus propios servicios de DHCP, enrutamiento y firewall. Así, un mismo controlador puede alojar la Network que administra UHM y otras redes gestionadas por una infraestructura independiente, pero UHM solo administra una Network. |
UniFi Controller
|
+-------------+--------------+
| |
Network: Default Other Networks
Third-Party Gateway VLANs / Networks
| |
Hotspot |
| |
Guest ESSID |
| |
v v
+-------------------+ +-------------------+
| UHM | | Third-party |
| | | infrastructure |
| 1 Network | | |
| 1 ESSID | | DHCP / Routing |
| 1 Hotspot | | Firewall / etc. |
| 1 IPv4 range | | |
+-------------------+ +-------------------+
|
v
├── DHCP
├── Firewall
├── Guest management
└── Optional:
├── Squid Proxy
├── Suricata
└── Unbound...
| Component | Used by | Purpose | Propósito |
|---|---|---|---|
| UniFi Network (self-hosted) | uhmd, uhmunifi.sh |
Captive portal SSID, vouchers, and the API Site must be Third-Party Gateway. Local admin account. See Instance above for the single-Network limitation | SSID de portal cautivo, vouchers, y el Site de la API debe ser Third-Party Gateway. Cuenta de admin local. Ver Instance arriba para la limitación de Network única |
| pydhcp | uhmd (verified at startup) |
uhmsetup.sh clones pydhcp and runs its interactive installer, pysetup.sh, which asks for the network settings and saves them to pydhcp.env. Installation is skipped if pydhcpd is already active. Only one DHCP server may be active |
uhmsetup.sh clona pydhcp y ejecuta su instalador interactivo, pysetup.sh, que solicita la configuración de red y la guarda en pydhcp.env. Si pydhcpd ya está activo, UHM omite la instalación. Solo debe haber un servidor DHCP activo |
| apache2 | panel, WPAD/PAC | Installed by uhmsetup.sh together with libapache2-mod-php. Serves the web panel on port 4048 and the PAC file on WPAD_PORT when the optional WPAD feature is enabled |
Lo instala uhmsetup.sh junto con libapache2-mod-php. Sirve el panel web en el puerto 4048 y el archivo PAC en WPAD_PORT cuando se activa la función opcional WPAD |
| git | uhmsetup.sh (install time only) |
Clones the pydhcp repository | Clona el repositorio de pydhcp |
| iptables + ipset | uhmiptables.sh |
uhmsetup.sh installs an initial configuration with IPv4 forwarding and NAT. It does not include the firewall rules that enforce ACLs; adapt and copy the reference rules over it |
uhmsetup.sh instala una configuración inicial con reenvío IPv4 y NAT. No incluye las reglas de firewall para aplicar las ACL: debes adaptar y copiar las reglas de referencia |
| bash, curl, jq | uhmd, uhmunifi.sh, uhmleases.sh |
Script execution, UniFi API access, JSON reading | Ejecución de scripts, acceso a la API de UniFi y lectura de datos JSON |
| openssl | uhmsetup.sh (install time only) |
Computes UNIFI_CERT_PIN from the controller's TLS certificate |
Calcula UNIFI_CERT_PIN a partir del certificado TLS del controlador |
| python3 | uhmleases.sh (runtime), uhmsetup.sh (install time) |
Range arithmetic: checks that SERVER_IP does not fall inside the block pool or the hotspot range, and that the hotspot range is inside the network and does not overlap pydhcp's pool |
Aritmética de rangos: verifica que SERVER_IP no caiga dentro del pool de bloqueo ni del rango del hotspot, y que el rango del hotspot esté dentro de la red y no se solape con el pool de pydhcp |
| coreutils, grep | all bash scripts in the project | Text/field parsing (MAC/IP/ACL lines, DHCP config, logs) | Parseo de texto/campos (líneas MAC/IP/ACL, config DHCP, logs) |
| sed | uhmd.sh, uhmleases.sh, uhmwatch.sh, uhmunifi.sh |
Direct edits to ACL and configuration files | Edición directa de archivos ACL y de configuración |
util-linux (flock) |
all bash scripts in the project | Prevents overlapping runs of the same script | Evita que se solapen dos ejecuciones del mismo script |
iproute2 (ip, ss) |
uhmsetup.sh (install time), uhmiptables.sh |
ss checks whether a port is already in use; ip link show verifies that WAN_IFACE exists before the NAT rule names it |
ss comprueba si un puerto ya está en uso; ip link show verifica que WAN_IFACE exista antes de que la regla NAT la nombre |
libc-bin (getent) |
uhmleases.sh |
Checks that the pydhcpd user and group exist |
Verifica que el usuario y grupo pydhcpd existan |
findutils (find) |
uhmsetup.sh |
Clears the install directory on uninstall, preserving bak/ |
Vacía el directorio de instalación al desinstalar, conservando bak/ |
procps (sysctl) |
uhmiptables.sh |
Enables IPv4 forwarding | Habilita el forwarding IPv4 |
systemd (systemctl) |
uhmd, uhmreload.sh, uhmwatch.sh, uhmleases.sh, uhmalert.sh, uhmtool.sh |
Manages/checks the uhmd/pydhcpd/UniFi services |
Gestiona/verifica los servicios uhmd/pydhcpd/UniFi |
| cron | uhmwatch.sh (mandatory, installed automatically) |
Runs the service supervisor every minute | Ejecuta cada minuto el supervisor de servicios |
| logrotate | uhmsetup.sh (writes /etc/logrotate.d/uhm) |
Rotates /var/log/uhm.log daily; without it the shared log grows without limit |
Rota /var/log/uhm.log a diario; sin él el log compartido crece sin límite |
| zip | tools/uhmbk.sh |
Creates a monthly compressed configuration archive under /etc/bak/uhm |
Crea cada mes un archivo comprimido con la configuración en /etc/bak/uhm |
| Component | Requirements | Purpose | Propósito |
|---|---|---|---|
| WPAD/PAC | apache2 (installed by UHM) and pydhcpd with DHCP option 252 support |
Optional proxy auto-configuration feature. The installer offers to enable it; when enabled, it publishes the PAC file through Apache and configures DHCP option 252 | Función opcional de configuración automática del proxy. El instalador ofrece activarla; al hacerlo, publica el archivo PAC mediante Apache y configura la opción DHCP 252 |
# Required packages -- uhmsetup.sh aborts if any is missing
sudo apt update
sudo apt install -y bash curl jq iptables ipset cron python3 openssl coreutils util-linux iproute2 grep sed systemd libc-bin findutils procps logrotate git zip
# Installed by uhmsetup.sh, not by hand:
# • pydhcp (DHCP backend) — https://github.com/maravento/pydhcp
# • apache2, libapache2-mod-php (panel and WPAD)squid is not a dependency of UHM. UHM neither installs it nor needs it to run. It is only detected: when WPAD is accepted,
uhmsetup.shlooks forsquid,squid-opensslorsquid3, reads the firsthttp_portfrom/etc/squid/squid.confand checks that something is listening on it. If it answers, the generatedwpad.pacpoints clients at that proxy; if not, the PAC returnsDIRECTand nothing else changes. The reference firewall ruleset (tools/uhmiptables_example.txt) does assume a proxy, which is one of the reasons it is not deployed as-is.squid no es una dependencia de UHM. UHM no lo instala ni lo necesita para funcionar. Solo lo detecta: cuando se acepta WPAD,
uhmsetup.shbuscasquid,squid-opensslosquid3, lee el primerhttp_portde/etc/squid/squid.confy comprueba que algo escuche en ese puerto. Si responde, elwpad.pacgenerado apunta a ese proxy; si no, el PAC devuelveDIRECTy nada más cambia. El ruleset de firewall de referencia (tools/uhmiptables_example.txt) sí asume un proxy, y esa es una de las razones por las que no se despliega tal cual.
To start, UHM must be able to log in to the UniFi controller, and
pydhcpdmust be active. If either remains unavailable after the grace period,uhmdexits. UHM also needs to finduhmreload.shto start. Ifuhmiptables.shis missing, the reload logs a warning and continues without applying firewall rules. If the script exists but fails when run, the reload stops and the firewall may be incomplete; the daemon can still run and classify clients.Para iniciar, UHM necesita que el controlador UniFi permita iniciar sesión y que
pydhcpdesté activo. Si alguno sigue sin estar disponible al terminar el período de gracia,uhmdtermina. También necesita encontraruhmreload.shpara arrancar. Si faltauhmiptables.sh, la recarga registra una advertencia y continúa sin aplicar las reglas del firewall. Si el script existe pero falla al ejecutarse, la recarga se interrumpe y el firewall puede quedar incompleto; aun así, el daemon puede seguir funcionando y clasificando clientes.
What UHM does
|
Lo que UHM hace
|
This is the structure of the repository after cloning it with git clone ... && cd uhm. It does not correspond to the structure of the installation. uhmsetup.sh is used only from the clone and is never deployed to the installed system. The files under core/ and tools/ are deployed by uhmsetup.sh into their respective subdirectories inside /etc/uhm/. That includes tools/uhmiptables_example.txt, deployed read-only and never executed, so the administrator can copy it over the placeholder without the clone. The files under config/ go to their system locations instead, not to /etc/uhm/: the unit to /etc/systemd/system/, the two vhosts to /etc/apache2/sites-available/ and the sudo rule to /etc/sudoers.d/. And web/ goes to /var/www/uhm, only if the panel is accepted. In other words, the clone holds the files needed to perform the installation, while /etc/uhm/ holds the files used by the running installation.
|
Esta es la estructura del repositorio después de clonarlo con git clone ... && cd uhm. No es la estructura del sistema instalado. uhmsetup.sh se utiliza únicamente desde el clon y nunca se despliega en el sistema instalado. El instalador copia los archivos de core/ y tools/ en sus respectivos subdirectorios de /etc/uhm/. También copia tools/uhmiptables_example.txt como archivo de solo lectura; no lo ejecuta. Así, puedes copiarlo sobre la configuración inicial del firewall sin conservar el clon. Los archivos de config/ se instalan en sus rutas del sistema, no en /etc/uhm/: la unidad de servicio en /etc/systemd/system/, los VirtualHost en /etc/apache2/sites-available/ y la regla de sudo en /etc/sudoers.d/. Los archivos de web/ se copian a /var/www/uhm solo si aceptas instalar el panel. En resumen, el clon contiene los archivos del instalador; /etc/uhm/ contiene los archivos que usa UHM una vez instalado.
|
uhm/ # as cloned -- see note above
├── acl/ # UHM's own data files -- empty templates in the repo,
│ # deployed once by uhmsetup.sh and never overwritten again
│ ├── uhm-auth.txt # authenticated clients, each with a voucher (fixed hotspot IP)
│ ├── uhm-grace.txt # clients still in the grace period, no voucher yet
│ └── uhm-queue.txt # MACs queued for lease removal, drained on the next run
│
├── config/ # server configuration, one directory per component --
│ # none of it is ever published under a web root
│ ├── service/
│ │ └── uhmd.service # systemd unit for uhmd
│ ├── uhmweb/
│ │ ├── uhmweb.conf # Apache vhost on port 4048
│ │ └── uhmweb.sudoers # sudo rule that lets www-data reach uhmtool.sh
│ └── wpad/
│ └── wpad.conf # Apache vhost on WPAD_PORT (default 18100)
│
├── core/ # the reload mechanism, plus uhmwatch -- UHM cannot
│ # function correctly without any of these four
│ ├── uhmd.sh # main daemon: polls the UniFi API and manages ACLs (systemd)
│ ├── uhmleases.sh # rebuilds pydhcpd.conf and manages DHCP leases/ACLs,
│ │ # with UniFi Hotspot support built in
│ ├── uhmreload.sh # helper called by uhmd after an ACL change -- runs
│ │ # uhmleases.sh, then reloads the affected services
│ └── uhmwatch.sh # mandatory service supervisor for uhmd, pydhcpd and the UniFi
│ # backend -- installed automatically by uhmsetup.sh
│ # with its own cron entry; lives here, not in tools/,
│ # because it's mandatory
│
├── tools/ # independent, optional utilities -- UHM runs
│ # fine without any of these
│ ├── uhmalert.sh # optional tool that monitors the log and sends
│ │ # notifications via ntfy.sh
│ ├── uhmbk.sh # backs up uhm's own files into /etc/bak/uhm,
│ │ # run monthly through cron
│ ├── uhmiptables.sh # firewall placeholder (IPv4 forwarding + NAT only)
│ │ # -- deployed only if missing, never overwritten
│ ├── uhmiptables_example.txt # full reference ruleset (ipsets, iptables, redirects)
│ │ # -- deployed read-only next to the placeholder;
│ │ # copy it over uhmiptables.sh and adapt it
│ ├── uhmtool.sh # JSON backend for the web interface -- reads the log,
│ │ # the ACL files and the UniFi API, and writes back an
│ │ # ACL file after validating it
│ └── uhmunifi.sh # audits UniFi clients and vouchers, and checks one
│ # MAC's live UniFi state
│
├── web/ # web interface -- deployed to /var/www/uhm only when
│ # the panel is accepted during install
│ ├── aclview/index.php # ACL tab: editor for the ACL lists
│ ├── logview/index.php # LogView tab: real-time viewer for uhmd
│ ├── toolview/index.php # Tool tab: local ACL and UniFi reports
│ ├── api.php # single endpoint, calls uhmtool.sh through sudo
│ └── index.html # panel shell: three tabs, light and dark theme
│
└── uhmsetup.sh # installer / updater / uninstaller (interactive);
# run from here, never deployed to /etc/uhm/
UHM integrates three independent components: UniFi, pydhcp and the iptables/ipset configuration defined by the administrator. Each one keeps its own ACLs and its own location. UHM reads and writes the ACLs in their respective locations and never moves, renames or relocates files belonging to another component.
|
UHM integra tres componentes independientes: UniFi, pydhcp y la configuración de iptables/ipset definida por el administrador. Cada uno mantiene sus propias ACL y su propia ubicación. UHM lee y escribe las ACL en sus respectivas ubicaciones y nunca mueve, renombra ni reubica archivos que pertenecen a otro componente.
|
/etc/uhm/acl/ # UHM's OWN data files (generated by this project;
# shipped as empty templates in the repo's acl/ folder,
# deployed once by uhmsetup.sh, never overwritten again)
├── uhm-auth.txt # voucher-authorized clients (fixed hotspot IP)
├── uhm-queue.txt # internal working file (uhmd.sh / uhmleases.sh only)
└── uhm-grace.txt # grace-period clients (no voucher yet)
/etc/acl/mac/ # pydhcp's namespace -- NOT generated by UHM
├── mac-limited.txt # user-maintained; UHM only reads it
└── mac-unlimited.txt # user-maintained; UHM only reads it
/etc/pydhcp/acl/ # pydhcp's own namespace -- NOT generated by UHM
└── blockdhcp.txt # permanently blocked MACs; pydhcp/pyleases.sh concept,
# reused (not owned) by uhmleases.sh
UHM works with ACLs belonging to three independent components: UHM, pydhcp and the administrator's iptables/ipset configuration. The paths ACL_MAC_PATH (/etc/acl/mac) and ACL_DHCP_PATH (/etc/pydhcp/acl), as well as the variables naming the files they contain, are configurable in uhm.env. This lets UHM respect the paths the administrator already uses for pydhcp and iptables, without imposing its own. uhm.env is located directly in /etc/uhm/. It is not inside /etc/uhm/acl/ because it is a configuration file, not a data list. The only ACL path that belongs to UHM is: /etc/uhm/acl/ That path is part of the installation and is kept or removed along with UHM, as applies during an update or an uninstall. Variable names The variables pointing at UHM's own three lists are named after the file they refer to and use the UHM_ prefix:
ACL_ prefix:
uhmd.sh and uhmleases.sh can create UHM's own three lists empty when they do not exist. UHM never creates blockdhcp.txt nor any mac-*.txt file, because those files belong to other components. If blockdhcp.txt does not exist, uhmd.sh aborts and states that it must be created through pysetup.sh, the installer of pydhcp.
|
UHM trabaja con ACL pertenecientes a tres componentes independientes: UHM, pydhcp y la configuración de iptables/ipset del administrador. Las rutas ACL_MAC_PATH (/etc/acl/mac) y ACL_DHCP_PATH (/etc/pydhcp/acl), así como las variables que indican los archivos que contienen, son configurables en uhm.env. Esto permite que UHM respete las rutas que el administrador ya utiliza para pydhcp e iptables, sin imponer rutas propias. uhm.env se encuentra directamente en /etc/uhm/. No está dentro de /etc/uhm/acl/ porque es un archivo de configuración, no una lista de datos. La única ruta de ACL que pertenece a UHM es: /etc/uhm/acl/ Esta ruta forma parte de la instalación y se conserva o elimina junto con UHM, según corresponda durante una actualización o desinstalación. Nombres de las variables Las variables que apuntan a las tres listas propias de UHM se nombran según el archivo al que hacen referencia y utilizan el prefijo UHM_:
ACL_:
uhmd.sh y uhmleases.sh pueden crear vacías las tres listas propias de UHM cuando no existen. En cambio, UHM nunca crea blockdhcp.txt ni ningún archivo mac-*.txt, porque esos archivos pertenecen a otros componentes. Si blockdhcp.txt no existe, uhmd.sh aborta e indica que debe ser creado mediante pysetup.sh, el instalador de pydhcp.
|
| 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. |
uhm-auth.txt |
3 | List operated by the UHM daemon. Designed for clients that entered with a valid UniFi voucher. May be subject to firewall, proxy and other restrictions. A malformed line aborts with ERROR. |
Lista operada por el demonio UHM. Está diseñada para los clientes que ingresan con voucher válido de UniFi. Puede estar sujeta a restricciones de firewall, proxy, etc. Una línea malformada aborta con ERROR. |
uhm-grace.txt |
0 | List operated by the UHM daemon. Designed for clients seen on the network that have not entered a voucher yet, during their grace period. Authorizes nothing on its own. A malformed line is dropped with INFO and the reload continues. |
Lista operada por el demonio UHM. Está diseñada para los clientes vistos en la red que aún no ingresan un voucher, durante su período de gracia. No autoriza nada por sí sola. Una línea malformada se elimina con INFO y el reload continúa. |
blockdhcp.txt |
0 | List managed by the pydhcp daemon and written by uhmleases.sh. It identifies clients that must not receive a DHCP lease. It grants no access by itself. A malformed line is logged and removed; the reload continues. |
Lista gestionada por el demonio pydhcp y escrita por uhmleases.sh. Identifica a los clientes que no deben recibir una concesión DHCP. No concede acceso por sí sola. Si una línea no tiene el formato esperado, se registra y elimina; la recarga continúa. |
uhm-queue.txt |
0 | Internal list used by the UHM daemon to hold MAC addresses whose DHCP leases must be removed during the next reload. The list is cleared after processing. It grants no access. A malformed line is logged and removed; the reload continues. |
Lista interna que usa el daemon de UHM para guardar las direcciones MAC cuyas concesiones DHCP deben retirarse en la próxima recarga. Se vacía después de procesarlas. No concede acceso. Si una línea no tiene el formato esperado, se registra y elimina; la recarga continúa. |
Lines starting with
#are treated as deactivated and get blocked. Only applies to the ACLs with Priority Level 1, 2 and 3.Las líneas que comienzan con
#se consideran desactivadas y serán bloqueadas. Solo aplica a las ACL con Priority Level 1, 2 y 3.
Before running uhmd, in the UniFi Network controller:
|
Antes de ejecutar uhmd, en el controlador UniFi Network:
|
Remote Access via unifi.ui.com
Acceso remoto vía unifi.ui.com
|
UHM can coexist with UniFi Remote Access. Remote Access can be enabled on a locally-administered self-hosted UniFi Network Server, by default Admin plus password, managed by UHM. The UniFi console is then available both locally and from https://unifi.ui.com, by default email plus password plus MFA Login Authentication, with no conflict for UHM. Enabling 2FA OTP, generated by an authenticator app, does break UHM's authentication against the UniFi API. |
UHM puede coexistir con UniFi Remote Access. Remote Access puede habilitarse en un UniFi Network self-hosted con administración local, por defecto Admin más contraseña, gestionado por UHM. La consola UniFi queda disponible tanto localmente como desde https://unifi.ui.com, por defecto correo más contraseña más MFA Login Authentication, sin conflicto con UHM. Activar 2FA OTP, generado por una aplicación autenticadora, sí rompe la autenticación de UHM contra la API de UniFi. |
UHM also coexists without conflict with Multi-Site Management enabled on the same console.
UHM también coexiste sin conflicto con Multi-Site Management activado en la misma consola.
Clone the repository with git clone and run uhmsetup.sh. The installer takes care of:
uhmsetup.sh takes them from /etc/pydhcp/pydhcp.env. UHM supports a single UniFi controller and a single guest SSID. Both are auto-detected through the UniFi API. How each one is resolved is explained below. At the end of the installation, the installer offers three independent optional components. Each prompt defaults to No:
In addition, pydhcp must be installed and running, and /etc/pydhcp/pydhcp.env must exist and hold the required values. UHM uses that file to obtain the network configuration instead of asking for it again during the installation.
|
Clona el repositorio con git clone y ejecuta uhmsetup.sh. El instalador se encarga de:
uhmsetup.sh los obtiene de /etc/pydhcp/pydhcp.env. UHM admite un solo controlador UniFi y un solo SSID de invitados. Ambos se autodetectan mediante la API de UniFi. El detalle de cómo se determina cada uno se explica más abajo. Al final de la instalación, el instalador ofrece tres componentes opcionales. Cada pregunta tiene No como opción predeterminada:
Además, pydhcp debe estar instalado y funcionando, y /etc/pydhcp/pydhcp.env debe existir y contener los valores necesarios. UHM utiliza ese archivo para obtener la configuración de red en lugar de solicitarla nuevamente durante la instalación.
|
git clone --depth=1 https://github.com/maravento/uhm.git
cd uhm
sudo bash uhmsetup.sh
The installer checks the required APT dependencies:
curl, jq, iptables, ipset, python3, openssl, coreutils, util-linux, iproute2, cron, grep, sed, systemd, libc-bin, findutils, procps and logrotate. If any of them is missing, the installation aborts. No dependency is installed automatically. The installation also aborts if pydhcp is not active. uhm.env holds only UHM's own variables. The network configuration of pydhcp stays in pydhcp.env. Every UHM component reads pydhcp.env first and uhm.env afterwards. This way any change made in pydhcp.env reaches UHM without reinstalling it, and the same variable is never stored in both files. The installer deploys:
systemctl enable and systemctl restart uhmd. No UHM file is copied to /etc/pydhcp.
|
El instalador verifica las dependencias APT requeridas:
curl, jq, iptables, ipset, python3, openssl, coreutils, util-linux, iproute2, cron, grep, sed, systemd, libc-bin, findutils, procps y logrotate. Si falta alguna, la instalación se aborta. Ninguna dependencia se instala automáticamente. También se aborta la instalación si pydhcp no está activo. uhm.env contiene únicamente las variables propias de UHM. La configuración de red de pydhcp permanece en pydhcp.env. Cada componente de UHM consulta primero pydhcp.env y después uhm.env. De esta forma, cualquier cambio realizado en pydhcp.env queda disponible para UHM sin necesidad de reinstalarlo, y una misma variable no se almacena en ambos archivos. El instalador despliega:
systemctl enable y systemctl restart uhmd. No se copian archivos de UHM a /etc/pydhcp.
|
systemd service uhmd.service runs UHM's main loop. The daemon performs a check every POLL_INTERVAL seconds, whose default value is 20 and is set in uhm.env. No crontab entry is registered. The daemon itself runs the safety-net reload when it applies. |
Servicio systemd uhmd.service ejecuta el ciclo principal de UHM. El daemon realiza una comprobación cada POLL_INTERVAL segundos, cuyo valor predeterminado es 20 y se configura en uhm.env. No se registra ninguna entrada de crontab. El propio daemon ejecuta internamente el reload de seguridad cuando corresponde. |
|
Controller and SSID resolution UHM supports exactly one UniFi controller and one guest SSID. For that reason, neither of them is entered by hand as free text. UniFi controller The installer uses SERVER_IP, taken from pydhcp.env and corresponding to the host's own LAN IP, to probe ports 8443 and 11443 with the UniFi credentials entered during the installation. If it finds the controller, it uses that connection directly. If it does not find it, the installation aborts. In that case, check the credentials and confirm that the UniFi controller is running on this same host before running the installation again. Guest SSID Once authenticated against the controller, the installer obtains the configured SSIDs through rest/wlanconf.
|
Resolución del controlador y del SSID UHM admite exactamente un controlador UniFi y un SSID de invitados. Por esta razón, ninguno de los dos se introduce manualmente como texto libre. Controlador UniFi El instalador utiliza SERVER_IP, obtenido de pydhcp.env y correspondiente a la IP LAN del propio host, para probar los puertos 8443 y 11443 con las credenciales de UniFi introducidas durante la instalación. Si encuentra el controlador, utiliza esa conexión directamente. Si no lo encuentra, la instalación se aborta. En ese caso, revise las credenciales y confirme que el controlador UniFi esté ejecutándose en este mismo host antes de volver a ejecutar la instalación. SSID de invitados Una vez autenticado en el controlador, el instalador obtiene los SSID configurados mediante rest/wlanconf.
|
|
Purpose of the daemon The daemon keeps the ACL lists up to date and guarantees that the reload chain stays active even when no client is connected. On every cycle, uhmd.sh checks how much time has passed since the last reload. If more than RELOAD_SAFETY_INTERVAL_SECONDS have elapsed —3600 seconds by default, that is, one hour— it forces a reload even if no ACL has changed. This periodic reload lets the grace entries that have expired move to blockdhcp.txt even on an idle network, where no new client would normally trigger a reload. uhmd.sh is the only component that invokes uhmreload.sh. There is no external cron entry to run the reload. Therefore, there are no two independent callers that could compete for the instance lock of uhmreload.sh.
|
Propósito del daemon El daemon mantiene actualizadas las listas ACL y garantiza que la cadena de reload continúe activa incluso cuando no hay clientes conectados. En cada ciclo, uhmd.sh comprueba cuánto tiempo ha pasado desde el último reload. Si han transcurrido más de RELOAD_SAFETY_INTERVAL_SECONDS —3600 segundos por defecto, es decir, una hora— fuerza un reload aunque ninguna ACL haya cambiado. Este reload periódico permite que las entradas de gracia que hayan expirado pasen a blockdhcp.txt incluso en una red sin actividad, donde ningún cliente nuevo provocaría normalmente un reload. uhmd.sh es el único componente que invoca uhmreload.sh. No existe ninguna entrada de cron externa para ejecutar el reload. Por tanto, no hay dos invocadores independientes que puedan competir por el lock de instancia de uhmreload.sh.
|
| Verify the daemon status with: | Verifique el estado del daemon con: |
systemctl status uhmd
journalctl -u uhmd -f|
The update replaces only the program files. It never modifies the existing configuration or the ACL data. Files that are updated:
tools/uhmiptables_example.txt is deployed read-only on every run: it is reference material, not customized data. tools/uhmiptables.sh is deployed only when it does not exist in the installation. Files and data that are not overwritten:
If the ACL files or the logrotate configuration are missing during an update, they are recreated empty and a WARNING is shown. If uhmiptables.sh is missing, the placeholder is deployed again. uhm.env gets a special treatment: with --update it is not created, not verified and not repaired. If it is missing, that situation is only detected during a fresh installation, without --update. Services during the update Before replacing the files, uhmd.service and uhmalert.service are stopped, if they are installed and active. At the end, only the services that were active before the update are started again. The cron entry used by uhmwatch is removed during that same window and registered again at the end. uhmwatch is not a systemd service. Nothing that was stopped or disabled before the update is started as a result of it. pydhcpd is neither stopped nor modified. pydhcp is an independent project and stopping it would interrupt the DHCP service of the whole LAN, not only the hotspot's. uhmreload cron The update removes any stale @hourly cron entry used to run uhmreload.sh. That external execution is no longer needed because the daemon performs the safety-net reload internally. Backup beforehand Before overwriting any file, the update runs uhmbk.sh. The backup writes a full ZIP of /etc/uhm to: /etc/bak/uhm/uhmbk_<YYYYMMDD_HHMM>.zip If uhmbk.sh is not installed, a warning is shown and the update continues.
|
La actualización reemplaza únicamente los archivos del programa. Nunca modifica la configuración ni los datos ACL existentes. Archivos que se actualizan:
tools/uhmiptables_example.txt se despliega en solo lectura en cada ejecución: es material de referencia, no datos personalizados. tools/uhmiptables.sh solo se despliega cuando no existe en la instalación. Archivos y datos que no se sobrescriben:
Si durante una actualización faltan los archivos ACL o la configuración de logrotate, se recrean vacíos y se muestra un WARNING. Si falta uhmiptables.sh, se vuelve a desplegar el placeholder. uhm.env tiene un tratamiento especial: con --update no se crea, no se verifica y no se repara. Si falta, esta situación solo se detecta durante una instalación nueva, sin --update. Servicios durante la actualización Antes de reemplazar los archivos, se detienen uhmd.service y uhmalert.service, si están instalados y activos. Al finalizar, se vuelven a iniciar únicamente los servicios que estaban activos antes de la actualización. La entrada de cron utilizada por uhmwatch se elimina durante esta misma ventana y se vuelve a registrar al finalizar. uhmwatch no es un servicio systemd. Los servicios que estaban detenidos o deshabilitados antes de actualizar permanecen así. pydhcpd no se detiene ni se modifica. pydhcp es un proyecto independiente y detenerlo interrumpiría el servicio DHCP de toda la LAN, no únicamente el del hotspot. Cron de uhmreload La actualización elimina cualquier entrada de cron @hourly residual utilizada para ejecutar uhmreload.sh. Esa ejecución externa ya no es necesaria porque el daemon realiza internamente el reload de seguridad. Copia de seguridad previa Antes de sobrescribir cualquier archivo, la actualización ejecuta uhmbk.sh. La copia de seguridad genera un archivo ZIP completo de /etc/uhm en: /etc/bak/uhm/uhmbk_<AAAAMMDD_HHMM>.zip Si uhmbk.sh no está instalado, se muestra un aviso y la actualización continúa.
|
cd uhm
sudo bash uhmsetup.sh --update|
The installer also allows UHM to be uninstalled. Before starting, it shows a detailed warning with everything that will be removed and asks for a single confirmation. If the administrator confirms the uninstall, the operation runs to the end without further questions. That initial confirmation authorizes the complete removal of UHM. The uninstall removes every file, service, configuration and other component belonging to UHM. The APT dependencies used by UHM ( curl, jq, iptables, ipset, etc.) are not uninstalled. The firewall rules and the ipsets created for UHM are not removed automatically either. They must be cleaned by hand, following the instructions included at the end of the removal summary. |
El instalador también permite desinstalar UHM. Antes de comenzar, muestra una advertencia detallada con todo lo que será eliminado y solicita una única confirmación. Si el administrador confirma la desinstalación, la operación continúa hasta el final sin realizar nuevas preguntas. La desinstalación comienza solo después de que confirmes la operación; entonces continúa hasta el final sin más preguntas. La desinstalación elimina todos los archivos, servicios, configuraciones y demás componentes propios de UHM. Las dependencias APT utilizadas por UHM ( curl, jq, iptables, ipset, etc.) no se desinstalan. Las reglas de firewall y los ipsets creados para UHM tampoco se eliminan automáticamente. Deben limpiarse manualmente siguiendo las instrucciones incluidas al final del resumen de desinstalación. |
cd uhm
sudo bash uhmsetup.sh --remove| # | Description (two confirmations up front, then unconditional) | Descripción (dos confirmaciones al inicio, luego incondicional) |
|---|---|---|
| 1 | Stop and disable uhmd.service and remove /etc/systemd/system/uhmd.service |
Detiene y deshabilita uhmd.service y elimina /etc/systemd/system/uhmd.service |
| 2 | Remove the @hourly cron entry for /etc/uhm/core/uhmreload.sh (or the pre-restructure /etc/uhm/tools/uhmreload.sh path, if upgrading from an older install) |
Elimina la entrada de cron @hourly para /etc/uhm/core/uhmreload.sh (o la ruta previa a la reestructuración /etc/uhm/tools/uhmreload.sh, si se actualiza desde una instalación anterior) |
| 3 | Remove the uhmwatch cron entry, and stop/disable/remove uhmalert.service if installed |
Elimina la entrada de cron de uhmwatch, y detiene/deshabilita/elimina uhmalert.service si está instalado |
| 4 | Remove the web interface: /var/www/uhm, its vhost, its sudo rule and its Listen directives, if installed |
Elimina la interfaz web: /var/www/uhm, su vhost, su regla de sudo y sus directivas Listen, si está instalada |
| 5 | Remove /etc/logrotate.d/uhm |
Elimina /etc/logrotate.d/uhm |
| 6 | Remove /etc/uhm/ and all its contents including uhm.env, ACL files and your uhmiptables.sh |
Elimina /etc/uhm/ y todo su contenido, incluyendo uhm.env, archivos ACL y su uhmiptables.sh |
| 7 | Remove /var/log/uhm.log, rotated archives, /var/log/uhmunifi.log, /var/log/uhmleases-failure.trace and /var/log/uhmiptables-failure.trace |
Elimina /var/log/uhm.log, los archivos rotados, /var/log/uhmunifi.log, /var/log/uhmleases-failure.trace y /var/log/uhmiptables-failure.trace |
| Path | Description | Descripción |
|---|---|---|
/etc/uhm/core/uhmd.sh |
Main daemon | Daemon principal |
/etc/systemd/system/uhmd.service |
Systemd service unit | Unidad de servicio systemd |
/etc/uhm/core/uhmreload.sh |
Reload coordinator | Script coordinador de recargas |
/etc/uhm/core/uhmleases.sh |
Hotspot-aware DHCP lease manager | Gestor de concesiones DHCP para el hotspot |
/etc/uhm/tools/uhmunifi.sh |
Audit tool | Herramienta de auditoría |
/etc/uhm/uhm.env |
Configuration (IPs, credentials, ports) | Configuración |
/etc/uhm/acl/uhm-grace.txt |
Grace-period clients (no voucher yet) — list operated by the daemon, not by the administrator; do not edit its contents manually | Clientes en período de gracia — lista operada por el daemon, no por el administrador; no debe editarse su contenido manualmente |
/etc/uhm/acl/uhm-auth.txt |
Authorized clients (active voucher) — list operated by the daemon, not by the administrator; do not edit its contents manually | Autorizados — lista operada por el daemon, no por el administrador; no debe editarse su contenido manualmente |
/etc/uhm/acl/uhm-queue.txt |
Lease removal queue — path set by the UHM_QUEUE config variable; internal working file for uhmd.sh/uhmleases.sh, not an ACL — do not edit its contents manually |
Cola de remociones de leases — la ruta la fija la variable de configuración UHM_QUEUE; archivo de trabajo interno de uhmd.sh/uhmleases.sh, no es una ACL — no debe editarse su contenido manualmente |
/var/log/uhm.log |
Log file (unified) | Archivo de log (unificado) |
uhmsetup.log |
Installer log, written in the directory uhmsetup.sh is run from and rewritten on each run. Kept out of /var/log/uhm.log so install, update and remove runs never mix with daily operation — and so their WARNING/ERROR lines never reach uhmalert.sh, which pushes a notification for every one it finds in uhm.log |
Log del instalador, escrito en el directorio desde el que se ejecuta uhmsetup.sh y reescrito en cada corrida. Se mantiene fuera de /var/log/uhm.log para que las corridas de instalación, actualización y desinstalación no se mezclen con la operación diaria — y para que sus líneas WARNING/ERROR nunca lleguen a uhmalert.sh, que envía una notificación por cada una que encuentra en uhm.log |
/etc/logrotate.d/uhm |
Logrotate config | Config de logrotate |
/etc/uhm/core/uhmwatch.sh |
Services watchdog (mandatory) | Supervisor de servicios (obligatorio) |
/run/uhmwatch/ |
Watchdog recovery-attempt timestamps — cleared on reboot, not persistent | Marcas de tiempo de intentos de recuperación del vigilante — se limpian en cada reinicio, no persisten |
/etc/uhm/tools/uhmtool.sh |
JSON data provider for the web interface | Proveedor de datos JSON de la interfaz web |
/var/www/uhm/ |
Web interface (optional) | Interfaz web (opcional) |
/etc/apache2/sites-available/uhmweb.conf |
Apache VirtualHost on port 4048 (optional) | VirtualHost de Apache en el puerto 4048 (opcional) |
/etc/sudoers.d/uhmweb |
Sudo rule for www-data (optional) |
Regla de sudo para www-data (opcional) |
|
UHM uses two kinds of backup, with different purposes and rules. Project backup It is a full copy of the UHM installation, intended for the administrator. It is stored in /etc/bak/uhm, its name carries a timestamp and up to 3 copies are kept. Only uhmbk.sh creates project backups. Run it by hand before applying changes, or let its monthly cron entry do it. Paths that do not exist are skipped with a notice. To restore, unzip the archive over /. Routine-operation backup It is a copy of one specific file that a script makes right before modifying it. Its purpose is to allow that change to be undone if needed. It is stored next to the original file, with the .bak extension: <file>.bak Only one copy is kept. Every new run overwrites the previous copy. The difference between both kinds of backup is not given by the number of copies nor by how long they are kept, but by what is backed up and why the backup is made: the project backup copies the whole installation for the administrator; the routine-operation backup copies one specific file as a safety measure before modifying it. |
UHM crea dos tipos de copia de seguridad, cada una con un propósito distinto. Copia de seguridad del proyecto Es una copia completa de la instalación de UHM, destinada al administrador. Se guarda en /etc/bak/uhm, incluye una marca de tiempo en el nombre y se conservan hasta 3 copias. Solo uhmbk.sh crea copias de seguridad del proyecto. Ejecútelo a mano antes de aplicar cambios, o deje que lo haga su entrada mensual de cron. Las rutas que no existen se omiten con un aviso. Para restaurar, descomprima el archivo sobre /. Copia previa a una modificación Es una copia de un archivo concreto que un script realiza inmediatamente antes de modificarlo. Su finalidad es permitir deshacer ese cambio si fuera necesario. Se guarda junto al archivo original, con la extensión .bak: <archivo>.bak Solo se conserva una copia. Cada nueva ejecución sobrescribe la copia anterior. La copia del proyecto conserva la instalación completa para el administrador. La copia previa a una modificación guarda un archivo específico para poder recuperarlo si el cambio causa problemas. |
| Path | Kind | Written by | Escrito por |
|---|---|---|---|
/etc/bak/uhm/uhmbk_<TIMESTAMP>.zip |
Project, up to 3 | uhmbk.sh, archiving /etc/uhm and uhm's own systemd/Apache/logrotate files |
uhmbk.sh, archivando /etc/uhm y los archivos propios de uhm en systemd/Apache/logrotate |
/etc/pydhcp/core/pydhcpd.conf.bak |
Routine, 1 copy | uhmleases.sh, before regenerating the config; restored automatically if pydhcpd then fails to start |
uhmleases.sh, antes de regenerar la configuración; se restaura sola si pydhcpd no arranca después |
uhmbk.shships withuhmand only archivesuhm's own files.pydhcp's configuration has its own separate backup tool,pydhcp/tools/pybk.sh, writing to/etc/bak/pydhcp.
uhmbk.shviene conuhmy solo archiva los archivos propios deuhm. La configuración depydhcptiene su propia herramienta independiente para crear copias de seguridad,pydhcp/tools/pybk.sh, que escribe en/etc/bak/pydhcp.
| Variable | Description | Descripción |
|---|---|---|
WAN_IFACE |
Not a uhm.env key. It is pydhcp's own shared key, written by pysetup.sh into /etc/pydhcp/pydhcp.env and read from there by every project that needs it. tools/uhmiptables.sh validates it with KEY CHECK and has no fallback for it; the reference ruleset keeps one |
No es una clave de uhm.env. Es una clave compartida propia de pydhcp, escrita por pysetup.sh en /etc/pydhcp/pydhcp.env y leída desde ahí por cada proyecto que la necesite. tools/uhmiptables.sh la valida con KEY CHECK y no tiene fallback para ella; el ruleset de referencia sí lo mantiene |
INTERFACESv4 |
pydhcp's own value -- the LAN interface pydhcpd listens on, read from /etc/pydhcp/pydhcp.env at runtime; read by tools/uhmiptables_example.txt as its LAN interface; the placeholder does not use it |
Valor propio de pydhcp -- la interfaz LAN en la que escucha pydhcpd, leída desde /etc/pydhcp/pydhcp.env en cada ejecución; usada por tools/uhmiptables_example.txt como su interfaz LAN; el placeholder no la usa |
SERVER_IP |
This server's LAN IP, read from /etc/pydhcp/pydhcp.env at runtime. It is also the DHCP server address and is used by uhmleases.sh and uhmiptables.sh. |
Dirección IP de este equipo en la LAN, leída desde /etc/pydhcp/pydhcp.env en cada ejecución. También es la dirección del servidor DHCP y la usan uhmleases.sh y uhmiptables.sh. |
UHM_INI_RANGE, UHM_END_RANGE |
First and last address of the fixed-IP range handed to voucher-authorized guests, as two complete IPv4 addresses -- same shape as pydhcp's own SERV_INI_RANGE_BLOCK/SERV_END_RANGE_BLOCK, so no netmask is assumed |
Primera y última dirección del rango de IP fijas que se entrega a los invitados autorizados por voucher, como dos direcciones IPv4 completas -- misma forma que el propio SERV_INI_RANGE_BLOCK/SERV_END_RANGE_BLOCK de pydhcp, así que no se asume ninguna máscara |
UHM_ESSID |
Guest SSID name; must match UniFi exactly | Nombre del SSID de invitados; debe coincidir exactamente con UniFi |
UNIFI_CONTROLLER_URL |
e.g. https://192.168.1.1:8443 |
ej. https://192.168.1.1:8443 |
UNIFI_USERNAME, UNIFI_PASSWORD |
Local UniFi admin | Admin local de UniFi |
UNIFI_SITE |
Defaults to default; update if the site was renamed |
Por defecto default; actualizar si el sitio fue renombrado |
UNIFI_TYPE |
Either unifi-os or classic — sets the API path, login endpoint, session cookie name, and CSRF extraction method used by uhmd.sh |
unifi-os o classic — define la ruta de la API, el endpoint de login, el nombre de la cookie de sesión y el método de extracción de CSRF que usa uhmd.sh |
UNIFI_CERT_PIN |
SHA-256 pin of the controller's TLS public key (format sha256//<base64>), computed by uhmsetup.sh at install time. Used by uhmd.sh with curl --pinnedpubkey to detect a swapped certificate; empty if openssl failed during setup, in which case the connection falls back to unpinned -k |
Pin SHA-256 de la clave pública TLS del controlador (formato sha256//<base64>), calculado por uhmsetup.sh durante la instalación. Usado por uhmd.sh con curl --pinnedpubkey para detectar un certificado reemplazado; vacío si openssl falló durante la instalación, en cuyo caso la conexión cae a -k sin pin |
UHM_RELOAD |
Path to uhmreload.sh |
Ruta a uhmreload.sh |
UHM_LEASES |
Path to uhmleases.sh, invoked by uhmreload.sh as its first step (default /etc/uhm/core/uhmleases.sh) |
Ruta a uhmleases.sh, invocado por uhmreload.sh como su primer paso (default /etc/uhm/core/uhmleases.sh) |
UHM_IPTABLES |
Path to the administrator's firewall script, invoked by uhmreload.sh as its second step (default /etc/uhm/tools/uhmiptables.sh) |
Ruta al script de firewall del administrador, invocado por uhmreload.sh como su segundo paso (default /etc/uhm/tools/uhmiptables.sh) |
UHM_LEASES_TIMEOUT_SECONDS |
Max seconds uhmreload.sh waits for uhmleases.sh before killing it (default 120) |
Segundos máximos que uhmreload.sh espera a uhmleases.sh antes de matarlo (default 120) |
UHM_IPTABLES_TIMEOUT_SECONDS |
Max seconds uhmreload.sh waits for uhmiptables.sh before killing it (default 60) |
Segundos máximos que uhmreload.sh espera a uhmiptables.sh antes de matarlo (default 60) |
SERV_MASK |
Network mask, read from pydhcp.env at runtime |
Máscara de red, leída desde pydhcp.env en cada ejecución |
SERV_SUBNET |
Network address, read from pydhcp.env at runtime |
Dirección de red, leída desde pydhcp.env en cada ejecución |
SERV_BROADCAST |
Broadcast address, read from pydhcp.env at runtime |
Dirección de broadcast, leída desde pydhcp.env en cada ejecución |
SERV_DNS |
DNS servers for clients, read from pydhcp.env at runtime |
Servidores DNS para clientes, leída desde pydhcp.env en cada ejecución |
SERV_INI_RANGE_BLOCK, SERV_END_RANGE_BLOCK |
DHCP pool range for new/unknown clients, read from pydhcp.env at runtime |
Rango del pool DHCP para clientes nuevos/desconocidos, leída desde pydhcp.env en cada ejecución |
ACL_PATH |
Base ACL directory, read from pydhcp.env at runtime |
Directorio base de ACL, leída desde pydhcp.env en cada ejecución |
ACL_MAC_PATH |
Managed MAC lists directory, read from pydhcp.env at runtime |
Directorio de listas de MAC gestionadas, leída desde pydhcp.env en cada ejecución |
ACL_DHCP_PATH |
DHCP-related ACL files directory, read from pydhcp.env at runtime |
Directorio de archivos ACL relacionados con DHCP, leída desde pydhcp.env en cada ejecución |
UHM_PATH |
UHM installation/data directory (default /etc/uhm) |
Directorio de instalación/datos de UHM (default /etc/uhm) |
ACL_MAC_LIMITED |
List of managed device MAC addresses whose traffic must use the proxy, read from pydhcp.env at runtime |
Lista de direcciones MAC de dispositivos gestionados cuyo tráfico debe pasar por el proxy, leída desde pydhcp.env en cada ejecución |
ACL_MAC_UNLIMITED |
Managed unrestricted MAC list, read from pydhcp.env at runtime |
Lista de MAC gestionadas sin restricciones, leída desde pydhcp.env en cada ejecución |
UHM_MACAUTH |
Active hotspot-authorized MAC list -- UHM's own (default /etc/uhm/acl/uhm-auth.txt) |
Lista de MAC autorizadas activas del hotspot -- propia de UHM (default /etc/uhm/acl/uhm-auth.txt) |
ACL_BLOCK_FILE |
Permanently blocked MAC list, read from pydhcp.env at runtime |
Lista de MAC bloqueadas permanentemente, leída desde pydhcp.env en cada ejecución |
PYDHCPD_LEASES |
pydhcpd's own leases file path, read from pydhcp.env at runtime; read by uhmd.sh and uhmleases.sh (default /etc/pydhcp/core/pydhcpd.leases) |
Ruta del archivo de leases de pydhcpd, leída desde pydhcp.env en cada ejecución; usada por uhmd.sh y uhmleases.sh (default /etc/pydhcp/core/pydhcpd.leases) |
UHM_GRACE |
Grace-period MAC list -- UHM's own (default /etc/uhm/acl/uhm-grace.txt) |
Lista de MAC en período de gracia -- propia de UHM (default /etc/uhm/acl/uhm-grace.txt) |
UHM_QUEUE |
Path to the internal queue file that uhmd.sh prepares and uhmleases.sh processes to safely remove DHCP leases (default /etc/uhm/acl/uhm-queue.txt) |
Ruta del archivo de cola que uhmd.sh prepara y uhmleases.sh procesa para retirar concesiones DHCP de forma segura (por defecto, /etc/uhm/acl/uhm-queue.txt) |
POLL_INTERVAL |
Daemon cycle interval in seconds (default 20) |
Intervalo del ciclo del daemon en segundos (default 20) |
RELOAD_SAFETY_INTERVAL_SECONDS |
Maximum interval between safety-net reloads (default 3600 seconds = 1 hour). It must be at least three times the sum of UHM_LEASES_TIMEOUT_SECONDS and UHM_IPTABLES_TIMEOUT_SECONDS, and never below 600 seconds; uhmd stops if either minimum is not met. |
Intervalo máximo entre recargas preventivas (por defecto, 3600 segundos = 1 hora). Debe ser al menos tres veces la suma de UHM_LEASES_TIMEOUT_SECONDS y UHM_IPTABLES_TIMEOUT_SECONDS, y nunca inferior a 600 segundos; uhmd detiene el inicio si no se cumplen ambos mínimos. |
STARTUP_GRACE_SECONDS |
Grace window (seconds) for uhmd.sh's initial UniFi login retry and its wait for pydhcpd to come up (default 120). Also read by uhmwatch.sh to give its own functional login check (uosserver.service/unifi.service) the same exemption during this window; uhmalert.sh has its own separate key, UHM_ALERT_QUIET_PERIOD_SECONDS |
Ventana de gracia (segundos) para el reintento inicial de login a UniFi de uhmd.sh y su espera a que pydhcpd arranque (default 120). También la lee uhmwatch.sh para darle a su propio chequeo funcional de login (uosserver.service/unifi.service) la misma excepción durante esta ventana; uhmalert.sh tiene su propia clave separada, UHM_ALERT_QUIET_PERIOD_SECONDS |
UHM_ALERT_QUIET_PERIOD_SECONDS |
Grace window (seconds) for suppressing uhmalert.sh connectivity alerts right after uhmd.service starts (default 120) |
Ventana de gracia (segundos) para suprimir alertas de conectividad de uhmalert.sh justo después de que arranca uhmd.service (default 120) |
RECOVERY_COOLDOWN_SECONDS |
Minimum time between recovery attempts on the same service. The attempt is recorded before restarting the service, whether the recovery succeeds or fails (default 600 seconds = 10 minutes). |
Tiempo mínimo entre intentos de recuperación del mismo servicio. El intento se registra antes de reiniciarlo, tanto si la recuperación funciona como si falla (por defecto, 600 segundos = 10 minutos). |
CLEANUP_INTERVAL |
pydhcp's own value -- DHCP pool lease time in seconds, read from pydhcp.env at runtime (default 60) |
Valor propio de pydhcp -- tiempo de lease del pool DHCP en segundos, leída desde pydhcp.env en cada ejecución (default 60) |
AUTHORIZED_LEASE_TIME |
pydhcp's own value -- DHCP lease time for authorized clients in seconds, read from pydhcp.env at runtime (default 2592000 = 30 days) |
Valor propio de pydhcp -- tiempo de lease DHCP para clientes autorizados en segundos, leída desde pydhcp.env en cada ejecución (default 2592000 = 30 días) |
QUARANTINE_DURATION |
pydhcp's own value -- seconds an IP is held out of the pool after a DHCPDECLINE or ping-check conflict, read from pydhcp.env at runtime; written into pydhcpd.conf as abandon-lease-time (default 60) |
Valor propio de pydhcp -- segundos que una IP se aparta del pool tras un DHCPDECLINE o un conflicto de ping-check, leída desde pydhcp.env en cada ejecución; escrito en pydhcpd.conf como abandon-lease-time (default 60) |
BLOCKDHCP_GRACE_SECONDS |
Time a new MAC can remain in grace without redeeming a voucher (default 86400 seconds = 24 hours). When the timer expires, uhmleases.sh adds it to blockdhcp.txt on the next reload, triggered by an ACL change or by the safety-net interval. |
Tiempo que una MAC nueva puede permanecer en el período de gracia sin canjear un voucher (por defecto, 86400 segundos = 24 horas). Al agotarse, uhmleases.sh la añade a blockdhcp.txt durante la siguiente recarga, que puede activarse por un cambio en las ACL o por el intervalo preventivo. |
WPAD_ENABLED |
pydhcp value: set to true to enable WPAD/PAC through DHCP option 252. Apache must serve wpad.pac on WPAD_PORT. Read from pydhcp.env at runtime (default false). |
Valor propio de pydhcp: true activa WPAD/PAC mediante la opción DHCP 252. Requiere que Apache sirva wpad.pac en WPAD_PORT. Se lee desde pydhcp.env en cada ejecución (por defecto, false). |
WPAD_PORT |
pydhcp value: TCP port used by the Apache VirtualHost serving wpad.pac (default 18100). The reference firewall rules also use this port to allow PAC access by ACL group. |
Valor propio de pydhcp: puerto TCP del VirtualHost de Apache que sirve wpad.pac (por defecto, 18100). Las reglas de firewall de referencia también usan este puerto para permitir el acceso al PAC según el grupo ACL. |
PING_CHECK_ENABLED |
pydhcp's own value -- false to disable pydhcpd ping-check before OFFER, set if ICMP is blocked, read from pydhcp.env at runtime (default true) |
Valor propio de pydhcp -- false para deshabilitar el ping-check de pydhcpd antes del OFFER, usar si ICMP está bloqueado, leída desde pydhcp.env en cada ejecución (default true) |
PING_TIMEOUT_SECONDS |
pydhcp's own value -- seconds to wait for the ICMP reply before giving up and sending the OFFER, read from pydhcp.env at runtime; written into pydhcpd.conf as ping-timeout (default 1) |
Valor propio de pydhcp -- segundos a esperar la respuesta ICMP antes de desistir y enviar el OFFER, leída desde pydhcp.env en cada ejecución; escrito en pydhcpd.conf como ping-timeout (default 1) |
UHM_NTFY_TOPIC |
ntfy.sh topic used by uhmalert.sh (optional component). Auto-generated by uhmalert.sh install; absent if uhmalert is not installed |
Topic de ntfy.sh que usa uhmalert.sh (componente opcional). Lo autogenera uhmalert.sh install; ausente si uhmalert no está instalado |
UHM_API_FAIL_THRESHOLD |
Consecutive failing cycles uhmalert.sh requires before alerting (default 3). Written by uhmalert.sh install |
Ciclos fallidos consecutivos que uhmalert.sh exige antes de alertar (default 3). Lo escribe uhmalert.sh install |
Every variable above that isn't strictly required (network/UniFi credentials) falls back to the default shown if missing from
uhm.env— scripts never fail silently or use an undocumented value.Toda variable de arriba que no sea estrictamente requerida (red/credenciales UniFi) usa el default mostrado si falta en
uhm.env— los scripts nunca fallan en silencio ni usan un valor no documentado.
Example /etc/pydhcp/pydhcp.env (written by pydhcp's own pysetup.sh). UHM reads these values from here at runtime and never copies them:
|
Ejemplo de /etc/pydhcp/pydhcp.env (lo escribe el propio pysetup.sh de pydhcp). UHM lee estos valores de aquí en cada ejecución y nunca los copia:
|
# =============================================================================
# PYDHCP
# /etc/pydhcp/pydhcp.env
# =============================================================================
# -- Daemon defaults (pydhcpd.py / init.d/pydhcpd / pywebmin.sh) --------------
DHCPDv4_CONF=/etc/pydhcp/core/pydhcpd.conf
DHCPDv4_BIN=/usr/bin/python3
DHCPDv4_SCRIPT=/etc/pydhcp/core/pydhcpd.py
PYDHCPD_LEASES=/etc/pydhcp/core/pydhcpd.leases
INTERFACESv4=eth1
DAEMON_USER=pydhcpd
DAEMON_GROUP=pydhcpd
# -- Network values (chosen by the administrator during install) --------------
SERVER_IP=192.168.0.10
SERV_SUBNET=192.168.0.0
SERV_BROADCAST=192.168.0.255
SERV_MASK=255.255.255.0
SERV_INI_RANGE_BLOCK=192.168.0.230
SERV_END_RANGE_BLOCK=192.168.0.239
SERV_DNS=8.8.8.8,1.1.1.1
# -- ACL paths, administrator's own lists (edited by hand) --------------------
ACL_PATH=/etc/acl
ACL_MAC_PATH=/etc/acl/mac
ACL_MAC_LIMITED=/etc/acl/mac/mac-limited.txt
ACL_MAC_UNLIMITED=/etc/acl/mac/mac-unlimited.txt
# -- ACL paths, pydhcp's own list (written by pyleases.sh) --------------------
ACL_DHCP_PATH=/etc/pydhcp/acl
ACL_BLOCK_FILE=/etc/pydhcp/acl/blockdhcp.txt
# -- Lease timers (pyleases.sh -> pydhcpd.conf pool/subnet directives) --------
CLEANUP_INTERVAL=60
AUTHORIZED_LEASE_TIME=2592000
QUARANTINE_DURATION=60
# -- Optional features (pyleases.sh -> pydhcpd.conf wpad/ping-check) ----------
WPAD_ENABLED=false
WPAD_PORT=18100
PING_CHECK_ENABLED=true
PING_TIMEOUT_SECONDS=1
# -- pydhcp-only features (no isc-dhcp-server equivalent) ---------------------
PING_CACHE_TTL_SECONDS=120
RATE_LIMIT_WINDOW_SECONDS=60
RATE_LIMIT_MAX=5
RESERVATION_TTL_SECONDS=30
# =============================================================================
Example /etc/uhm/uhm.env (as written by uhmsetup.sh). Holds only UHM's own keys; pydhcp's values stay in the file above. uhmalert.sh install appends the last block.
|
Ejemplo de /etc/uhm/uhm.env (como lo escribe uhmsetup.sh). Contiene solo las claves propias de UHM; los valores de pydhcp se quedan en el archivo de arriba. uhmalert.sh install agrega el último bloque.
|
# =============================================================================
# UHM
# /etc/uhm/uhm.env
# =============================================================================
# -- UniFi keys ---------------------------------------------------------------
# Guest SSID
UHM_ESSID=EXAMPLE_SSID
# Unifi Access
UNIFI_CONTROLLER_URL=https://192.168.0.10:11443
UNIFI_USERNAME=admin
UNIFI_PASSWORD=mypass
UNIFI_SITE=default
# Unifi type (classic or unifi-os)
UNIFI_TYPE=unifi-os
# Cert
UNIFI_CERT_PIN=sha256//AbCdEfGhIjKlMnOpQrStUvWxYz0123456789ABCDE=
# -- Hotspot keys -------------------------------------------------------------
# Hotspot Range
UHM_INI_RANGE=192.168.0.180
UHM_END_RANGE=192.168.0.220
# Daemon timers (UHM's own)
POLL_INTERVAL=20
STARTUP_GRACE_SECONDS=120
RELOAD_SAFETY_INTERVAL_SECONDS=3600
BLOCKDHCP_GRACE_SECONDS=86400
RECOVERY_COOLDOWN_SECONDS=600
# -- Scripts ------------------------------------------------------------------
UHM_RELOAD=/etc/uhm/core/uhmreload.sh
UHM_LEASES=/etc/uhm/core/uhmleases.sh
UHM_IPTABLES=/etc/uhm/tools/uhmiptables.sh
# Timeouts (uhmd -> uhmreload -> uhmleases.sh/uhmiptables.sh)
UHM_LEASES_TIMEOUT_SECONDS=120
UHM_IPTABLES_TIMEOUT_SECONDS=60
# -- ACLs (UHM's own; read by uhmd.sh / uhmleases.sh) -------------------------
UHM_PATH=/etc/uhm
UHM_GRACE=/etc/uhm/acl/uhm-grace.txt
UHM_MACAUTH=/etc/uhm/acl/uhm-auth.txt
UHM_QUEUE=/etc/uhm/acl/uhm-queue.txt
# =============================================================================
# =============================================================================
# UHM ALERT
# =============================================================================
UHM_NTFY_TOPIC=uhm-alert-x7k2m9qv
UHM_API_FAIL_THRESHOLD=3
UHM_ALERT_QUIET_PERIOD_SECONDS=120
# =============================================================================New keys added later (e.g. by
uhmalert.sh install, or a backfill frompyleases.sh/pysetup.shon an older install) arrive as a complete block — its own# =====...=====opening and closing lines included — appended right after the last delimiter already in the file, so the file always ends on a delimiter.Las claves que se agregan después (por ejemplo con
uhmalert.sh install, o un relleno depyleases.sh/pysetup.shen una instalación anterior) llegan como un bloque completo — con sus propias líneas# =====...=====de apertura y cierre — añadido justo después del último delimitador que ya haya en el archivo, de modo que el archivo siempre termina en un delimitador.
The web interface is optional. The installer asks whether you want to install it. Apache publishes it through a VirtualHost on port 4048. It is deployed to: /var/www/uhm The interface has three tabs:
www-data, which does not read or write root-owned files directly. The interface gets its data from tools/uhmtool.sh, which runs with limited privileges through the sudo rule at: /etc/sudoers.d/uhmweb The interface is accessible only from the server itself or from an address in the LAN range. |
La interfaz web es opcional; el instalador te pregunta si quieres instalarla. Apache la publica mediante un VirtualHost en el puerto 4048. Se despliega en: /var/www/uhm La interfaz tiene tres pestañas:
www-data, un usuario que no lee ni escribe directamente archivos propiedad de root. La interfaz obtiene los datos mediante tools/uhmtool.sh, que se ejecuta con permisos controlados por la regla de sudo definida en: /etc/sudoers.d/uhmweb Solo se puede acceder a la interfaz desde el propio servidor o desde una dirección del rango de la red local. |
Panel header and tab bar
You can open each view in two ways. The URLs http://localhost:4048/?tab=logview, ?tab=aclview, and ?tab=toolview show the selected tab inside the panel. The paths http://localhost:4048/logview/, /aclview/, and /toolview/ open each module without the tab bar. Both options work.
|
Puedes abrir cada vista de dos maneras. Las direcciones http://localhost:4048/?tab=logview, ?tab=aclview y ?tab=toolview muestran la pestaña dentro del panel. Las rutas http://localhost:4048/logview/, /aclview/ y /toolview/ abren cada módulo sin la barra de pestañas. Ambas opciones funcionan.
|
Important
|
Importante
|
LogView — real-time viewer for uhmd
LogView lets you follow /var/log/uhm.log in real time. At each polling interval, it requests only the bytes added since the previous query instead of reloading the whole file. It also detects log rotation.
|
LogView permite consultar /var/log/uhm.log en tiempo real. En cada sondeo solicita por AJAX los bytes añadidos desde la consulta anterior, en lugar de volver a cargar todo el archivo. También detecta la rotación del registro.
|
| Feature | Description | Descripción |
|---|---|---|
| Live polling | AJAX polling by byte offset (1s–30s configurable). Never stalls on log rotation. | Polling AJAX por byte offset (1s–30s configurable). No se atasca con la rotación de logs. |
| Level indicators | Color-coded indicators, one distinct color per level: INFO (#d1ecf1/#0c5460), WARNING (#fff3cd/#856404), ERROR (#f8d7da/#721c24), STATUS (#e2e3e5/#383d41). |
Indicadores con colores, uno distinto por nivel: INFO (#d1ecf1/#0c5460), WARNING (#fff3cd/#856404), ERROR (#f8d7da/#721c24), STATUS (#e2e3e5/#383d41). |
| Cycle stats bar | Reads the latest stats line and shows counts for vouchers, authorized clients, grace-period clients, new authorizations, and revocations. | Lee la última línea de estadísticas y muestra los contadores de vouchers, autorizados, en gracia, autorizaciones nuevas y revocaciones. |
| Service status | Shows the PID, uptime, and memory use reported by systemctl status uhmd. |
Muestra el PID, el tiempo activo y el uso de memoria de systemctl status uhmd. |
LogView toolbar — search box, Full log, filters, interval, Reload and LIVE indicator
ACLView — editor for the ACL lists
|
Editor for the ACL lists. The selector lists the four files defined in uhm.env and pydhcp.env —uhm-auth, uhm-grace, uhm-queue, and blockdhcp— plus all mac-*.txt files in ACL_MAC_PATH. The editor cannot access other paths. Each line is validated against the format and IP requirements for its file. A malformed line rejects the whole save and reports its line number, preventing format errors that would abort the daemon's reload chain from reaching disk. The previous content is kept as <file>.bak. Editing is intended for the administrator-managed mac-*.txt lists. blockdhcp is pydhcp's block list; the administrator may remove a MAC from it to let that device reenter. The uhm-* lists are maintained by the daemon and uhmleases.sh; their contents may be rewritten or drained during normal operation. Avoid editing them by hand: any alteration may make the daemon abort the reload until the file is corrected. Conflicting active reservations in uhm-auth.txt — for example, one MAC assigned to different reservations or one IP assigned to different MACs — abort the reload before pydhcp is stopped, until the file is corrected.
|
Editor de las listas ACL. El selector permite elegir los cuatro archivos definidos en uhm.env y pydhcp.env —uhm-auth, uhm-grace, uhm-queue y blockdhcp— y todos los archivos mac-*.txt de ACL_MAC_PATH. El editor no permite acceder a otras rutas. Cada línea se valida según el formato y los requisitos de IP de su archivo. Una línea malformada rechaza el guardado completo e informa su número, para evitar que errores de formato que abortarían la cadena de recarga del daemon lleguen al disco. El contenido anterior se conserva como <archivo>.bak. La edición está pensada para las listas mac-*.txt, que son propiedad del administrador. blockdhcp es la lista de bloqueo de pydhcp; el administrador puede quitar una MAC para levantar el bloqueo y permitir que vuelva a entrar al sistema. Las listas uhm-* son gestionadas exclusivamente por el daemon de UHM y uhmleases.sh. Su contenido puede reescribirse o drenarse durante la operación normal. Evite editarlas a mano: cualquier alteración puede provocar que el daemon aborte la recarga hasta que se corrija el archivo. Las reservas activas en conflicto dentro de uhm-auth.txt —por ejemplo, una MAC asignada a reservas distintas o una misma IP asignada a MAC diferentes— abortan la recarga antes de detener pydhcp, hasta que se corrija el archivo.
|
ToolView — local ACL and UniFi reports
All reports are read-only and generated by tools/uhmtool.sh. Local ACL — The Check MAC, Grace period, Consistency check and Search by IP or hostname options read the local DHCP and ACL files: uhm-auth.txt, uhm-grace.txt, blockdhcp.txt, mac-*.txt and pydhcpd.leases. UniFi — The Connection status, Authorized, Vouchers, Guest sessions and Unauthorized options query the UniFi controller directly. To check a MAC's current state from the terminal —including ESSID, authorization, is_guest, IP, hostname, and voucher code— run the Check MAC option in uhmunifi.sh. Operations that modify information, such as deleting or revoking vouchers, are performed from uhmunifi.sh in the terminal. This panel is for consultation only and makes no changes.
|
Todos los reportes son de solo lectura y se generan mediante tools/uhmtool.sh. ACL local — Las opciones Check MAC, Grace period, Consistency check y Search by IP or hostname consultan los archivos locales de DHCP y ACL: uhm-auth.txt, uhm-grace.txt, blockdhcp.txt, mac-*.txt y pydhcpd.leases. UniFi — Las opciones Connection status, Authorized, Vouchers, Guest sessions y Unauthorized consultan directamente el controlador UniFi. Para consultar desde la terminal el estado actual de una MAC —ESSID, autorización, is_guest, IP, nombre del host y código del voucher— ejecuta la opción Check MAC de uhmunifi.sh. Las operaciones que modifican información, como borrar o revocar vouchers, se realizan desde uhmunifi.sh en la terminal. Este panel es exclusivamente de consulta y no realiza cambios.
|
Report selector
To change the configuration, edit /etc/uhm/uhm.env. To start over, uninstall UHM with uhmsetup.sh --remove and then run the installer again. Deleting only the configuration file is not enough; the installer will not run again while the deployed scripts remain.
|
Para cambiar la configuración, edita directamente /etc/uhm/uhm.env. Si quieres empezar de cero, primero desinstala UHM con uhmsetup.sh --remove y luego ejecuta de nuevo el instalador. Borrar solo el archivo de configuración no basta: el instalador no vuelve a ejecutarse mientras sigan instalados los scripts.
|
# Edit any value (credentials, interfaces, range, ports, SSID, etc.)
sudo nano /etc/uhm/uhm.env
# Or: force a fresh interactive setup
cd uhm && sudo bash uhmsetup.sh --remove
sudo bash uhmsetup.sh| (For full uninstall, see the Remove section above.) | (Para desinstalar por completo, vea la sección Remove más arriba.) |
The daemon executes a full cycle every POLL_INTERVAL seconds (default 20, configured in uhm.env). Each cycle has 11 steps. Two additional mechanisms run within the cycle; see «Independent Mechanisms» below.
|
El daemon ejecuta un ciclo completo cada POLL_INTERVAL segundos (default 20, configurado en uhm.env). Cada ciclo consta de 11 pasos. Además, dentro del ciclo operan dos mecanismos independientes, que se explican en la sección «Independent Mechanisms».
|
mac-*.txt change watcher (independent, not a numbered step): every cycle, right after snapshot, fingerprints all mac-*.txt files with a combined md5 (existence + content, no MAC/status parsing) and compares it to the previous cycle's. If it changed, the reload isn't triggered immediately — it's flagged for the reload step to pick up next cycle, so it never causes a second, separate uhmreload.sh invocation in the same run as one already triggered by the ACL files above.
An edit produces two log messages one cycle apart because they mark separate events, not a duplicate: 2026-07-23 22:01:28 INFO: mac-*.txt changed, reload next cycle2026-07-23 22:01:31 INFO: mac-*.txt changed, reloading now2026-07-23 22:01:31 INFO: invoking /etc/uhm/core/uhmreload.sh
The first line is the watcher noticing the change (this cycle); the second is the reload step actually acting on it (next cycle), immediately followed by the actual invocation. Seeing only the first without a follow-up second line one cycle later would itself be a sign something is wrong. authorize_managed_macs This independent mechanism runs immediately after revoke and reuses the stat/sta data already fetched for that cycle. For each active MAC of mac-*.txt that stat/sta currently reports as authorized=false, it calls UniFi's authorize-guest. The duration is derived from AUTHORIZED_LEASE_TIME / 60, that is, the same lease time pydhcp already gives those devices, 30 days by default. This exists because on a WLAN configured as Guest/Hotspot the AP keeps a client at the captive portal according to its own per-client authorized flag in UniFi. That happens regardless of the fixed-address DHCP bypass of pydhcpd and of the firewall rules of uhmiptables.sh. It was confirmed with direct queries to stat/sta showing is_guest=true and authorized=false for a mac-*.txt device with an otherwise correct fixed IP. It only touches UniFi's own state, never uhm-auth.txt nor any local ACL. It is self-repairing by design: it keeps no separate "already authorized" cache, so it authorizes the device again on its own if the UniFi state ever decays.
|
Vigilante de cambios en mac-*.txt (mecanismo independiente, no es un paso numerado): después de snapshot, compara la existencia y el contenido de esos archivos con el ciclo anterior mediante una huella MD5 combinada. No interpreta las MAC ni sus estados. Si detecta un cambio, lo marca y el paso reload lo procesa en el ciclo siguiente; así evita iniciar una segunda recarga independiente en un mismo ciclo.
Una edición produce dos mensajes de registro, separados por un ciclo, porque indican eventos distintos: 2026-07-23 22:01:28 INFO: mac-*.txt changed, reload next cycle2026-07-23 22:01:31 INFO: mac-*.txt changed, reloading now2026-07-23 22:01:31 INFO: invoking /etc/uhm/core/uhmreload.sh
La primera línea indica que el supervisor detectó el cambio. La segunda muestra que el paso de recarga lo procesó en el ciclo siguiente; después aparece la invocación de `uhmreload.sh`. Si no aparece ese segundo mensaje, puede haber un problema. authorize_managed_macs Este mecanismo independiente se ejecuta justo después de revoke y reutiliza los datos de stat/sta que ya se obtuvieron en ese ciclo. Para cada MAC activa de mac-*.txt que stat/sta reporta como authorized=false, llama a authorize-guest de UniFi. La duración se deriva de AUTHORIZED_LEASE_TIME / 60, es decir, el mismo lease time que pydhcp ya da a esos dispositivos, 30 días por defecto. Esto existe porque en una WLAN configurada como Guest/Hotspot el AP mantiene al cliente en el portal cautivo según su propio flag authorized por cliente en UniFi. Ocurre con independencia del bypass DHCP de dirección fija de pydhcpd y de las reglas de firewall de uhmiptables.sh. Se confirmó con consultas directas a stat/sta que mostraban is_guest=true y authorized=false para un dispositivo de mac-*.txt con una IP fija por lo demás correcta. Solo toca el estado propio de UniFi, nunca uhm-auth.txt ni ninguna ACL local. Es autorreparable por diseño: no mantiene una caché aparte de "ya autorizado", así que vuelve a autorizar el dispositivo por su cuenta si el estado de UniFi decae.
|
|
Client flow A new client connecting to the SSID receives a DHCP lease from pydhcpd's pool. On the new-clients step, which runs on every POLL_INTERVAL cycle, uhmd reads pydhcpd.leases directly and writes the MAC into uhm-grace.txt along with a timestamp. That write is what triggers the reload. The reload runs uhmleases.sh, which performs the classification, the expiration and the blocking. From there, the MAC can follow two paths:
uhm-auth.txt. It is not kept in any other location. On reconnecting it is treated as a new client and goes back to uhm-grace.txt with a fresh grace timer. blockdhcp.txt has two exits: removing the entry by hand, or adding the MAC to a mac-*.txt file.
Record format a;MAC;IP;HOSTNAME;END_TIME_EPOCH; in uhm-auth.txt. a;MAC;IP;HOSTNAME;FIRST_SEEN_EPOCH; in uhm-grace.txt. The leading a means "active" and marks a well-formed entry. Any other leading character makes the line malformed. There is no opposite value. To deactivate an entry, comment out the whole line by adding # at the beginning. Do not edit the a itself. In uhm-auth.txt, commenting out a line only changes its treatment at the DHCP level: the client goes from a fixed address to the blockdhcp class, the same as a commented entry in mac-*.txt. Commenting out a line does not pause END_TIME_EPOCH. The expire step removes the line once the voucher's time is up, whether the line is active or commented.
Malformed uhm-grace.txt lines: uhmleases.sh's expire_grace_entries() discards, rather than keeps, any line with a bad status/MAC/epoch field. This is intentional: the only writer of this file always writes a valid entry, so a dropped MAC is simply re-added correctly on its next DHCP lease renewal — keeping a malformed line instead would block that self-repair, since the file's own MAC-match check would treat it as already tracked and never write a fresh, valid entry for it.
Auth resilience: the CSRF token is extracted from the UniFi OS JWT payload ( csrfToken field, unifi-os) or from the response header (classic) after login, and persisted to /run/uhmd_session so it survives across $(...) subshell boundaries. On HTTP 401 from any API call, the daemon re-authenticates once and retries automatically.
Re-authorizing a client from the UniFi UI After a client has been revoked, that is, after UniFi reported it as authorized=false, re-authorizing it from the UniFi UI takes one extra cycle to take effect. That is one POLL_INTERVAL, 20 seconds with the default value. This is not a delay in UniFi. It is the order of the daemon's own cycle. The sessions step (7) runs before stat/sta is queried for the revoke step (8). The record that blocks the re-authorization is therefore cleared only after sessions has already run, and the client is picked up on the following cycle. That order is deliberate and is documented in run_cycle. Querying stat/sta earlier would let a stale reading undo a voucher redeemed moments before. Redeeming a new voucher is not affected. It carries a different end_time and is honoured on the very next cycle.
|
Flujo del cliente Un cliente nuevo que se conecta al SSID recibe un lease DHCP del pool de pydhcpd. En el paso de clientes nuevos, que se ejecuta en cada ciclo de POLL_INTERVAL, uhmd lee directamente pydhcpd.leases y escribe la MAC en uhm-grace.txt junto con una marca de tiempo. Esa escritura es la que dispara el reload. El reload ejecuta uhmleases.sh, que realiza la clasificación, la expiración y el bloqueo. A partir de ahí, la MAC puede seguir dos caminos:
uhm-auth.txt. No se conserva en ninguna otra ubicación. Al reconectarse se trata como un cliente nuevo y vuelve a uhm-grace.txt con un contador de gracia nuevo. blockdhcp.txt tiene dos salidas: eliminar la entrada manualmente, o incorporar la MAC a un archivo mac-*.txt.
Formato de registro a;MAC;IP;HOSTNAME;END_TIME_EPOCH; en uhm-auth.txt. a;MAC;IP;HOSTNAME;FIRST_SEEN_EPOCH; en uhm-grace.txt. La a inicial significa "active" y marca una entrada bien formada. Cualquier otro carácter inicial hace que la línea esté malformada. No existe un valor opuesto. Para desactivar una entrada, comente la línea completa agregando # al inicio. No edite la a. En uhm-auth.txt, comentar una línea solo cambia su tratamiento a nivel DHCP: el cliente pasa de dirección fija a la clase blockdhcp, igual que una entrada comentada de mac-*.txt. Comentar una línea no detiene END_TIME_EPOCH. El paso expire elimina la línea una vez cumplido el tiempo del voucher, esté activa o comentada.
Líneas malformadas en uhm-grace.txt: expire_grace_entries() de uhmleases.sh descarta, en vez de conservar, cualquier línea con status/MAC/epoch inválido. Es intencional: el único proceso que escribe este archivo siempre escribe una entrada válida, así que una MAC descartada simplemente se vuelve a agregar correctamente en su siguiente renovación de lease DHCP — conservar la línea malformada en cambio bloquearía esa autoreparación, porque el chequeo de coincidencia por MAC del archivo la trataría como ya rastreada y nunca escribiría una entrada nueva y válida para ella.
Resiliencia de auth: el token CSRF se extrae del payload JWT de UniFi OS (campo csrfToken, unifi-os) o del header de respuesta (classic) tras el login, y se persiste en /run/uhmd_session para que sobreviva el límite de subshells $(...). Ante HTTP 401 de cualquier llamada API, el daemon re-autentica una vez y reintenta automáticamente.
Reautorizar un cliente desde la UI de UniFi Después de que un cliente ha sido revocado, es decir, después de que UniFi lo reportó como authorized=false, reautorizarlo desde la UI de UniFi tarda un ciclo extra en surtir efecto. Ese ciclo es un POLL_INTERVAL, 20 segundos con el valor por defecto. No es una demora de UniFi. Es el orden del propio ciclo del daemon. El paso sessions (7) se ejecuta antes de que se consulte stat/sta para el paso revoke (8). Por eso el registro que bloquea la reautorización solo se descarta cuando sessions ya se ejecutó, y el cliente se recoge en el ciclo siguiente. Ese orden es deliberado y está documentado en run_cycle. Consultar stat/sta antes permitiría que una lectura obsoleta deshiciera un voucher canjeado instantes atrás. Canjear un voucher nuevo no se ve afectado. Trae otro end_time y se respeta en el ciclo inmediatamente siguiente.
|
The firewall is managed independently by the administrator via /etc/uhm/tools/uhmiptables.sh (see Scope), invoked by uhmreload.sh after every ACL change. The script flushes and rebuilds all ipsets and iptables rules from scratch on each run. Variables are loaded exclusively from uhm.env — no hardcoded network-specific values (interfaces, IPs, DNS). The UniFi ports listed below are fixed protocol requirements, not environment-specific, and are intentionally hardcoded.
The exact ipsets, rule order, and redirects are defined in tools/uhmiptables_example.txt — read that file directly rather than a copy here, since it changes independently of this document and a duplicated excerpt would inevitably drift out of sync with the real rules.
Note: uhmiptables.sh is invoked automatically by uhmreload.sh — never run it manually during normal operation. The reference ruleset flushes ALL iptables rules and ipsets on every run; the placeholder touches only its own two chains. Keys are read at runtime from /etc/pydhcp/pydhcp.env first and /etc/uhm/uhm.env after, and validated by KEY CHECK before any rule is applied.
Placeholder uhmsetup.sh deploys tools/uhmiptables.sh as a placeholder: IPv4 forwarding and NAT, nothing else. Ubuntu does neither by default, and without them LAN clients get a lease but reach nothing. Its rules live in two dedicated chains, UHM_NAT and UHM_FWD, flushed and rebuilt on every run so they never pile up. UHM_FWD exists because enabling forwarding in the kernel is not enough when the FORWARD policy is DROP. Nothing outside those two chains is touched and no policy is changed, so a firewall managed by other means stays intact. The placeholder does not redirect to a proxy, does not filter ports, does not bind MAC to IP and does not build any ipset. Access control still applies: UHM enforces it at the DHCP layer, through the blockdhcp deny class uhmleases.sh writes into pydhcpd.conf. For firewall-level enforcement, copy tools/uhmiptables_example.txt over this file and adapt it: back up the placeholder as uhmiptables.sh.bak, copy the example over uhmiptables.sh, and restore mode 750. Read it through before using it: it assumes a squid proxy on this host, and its rules for the limited and hotspot classes send traffic to it. The file is deployed only when it is missing and is never overwritten afterwards, since it becomes the administrator's own file once customized. Client classification into grace, authorized and blocked is done by uhmd. Blocked MACs are denied a lease by pydhcpd. The captive portal is enforced by UniFi's own per-client authorized flag. The three keep working independently of this file. If the file is missing, uhmreload.sh logs a warning and continues instead of treating it as a reload failure. See uhmreload in the CORE section for how the failure of this script, and of uhmleases.sh, is handled.
|
El firewall es gestionado independientemente por el administrador vía /etc/uhm/tools/uhmiptables.sh (ver Scope), invocado por uhmreload.sh tras cada cambio de ACL. El script vacía y reconstruye todos los ipsets y reglas iptables desde cero en cada ejecución. Las variables se cargan exclusivamente desde uhm.env — sin valores hardcodeados específicos del entorno (interfaces, IPs, DNS). Los puertos de UniFi listados abajo son requisitos fijos de protocolo, no específicos del entorno, y están hardcodeados intencionalmente.
Los ipsets exactos, el orden de reglas y las redirecciones están definidos en tools/uhmiptables_example.txt — consulte ese archivo directamente en vez de una copia aquí, ya que cambia independientemente de este documento y un extracto duplicado inevitablemente quedaría desincronizado de las reglas reales.
Nota: uhmiptables.sh es invocado automáticamente por uhmreload.sh — nunca ejecutarlo manualmente durante operación normal. El ruleset de referencia vacía TODAS las reglas iptables e ipsets en cada ejecución; el placeholder solo toca sus dos cadenas propias. Las claves se leen en tiempo de ejecución desde /etc/pydhcp/pydhcp.env primero y /etc/uhm/uhm.env después, y las valida KEY CHECK antes de aplicar ninguna regla.
Placeholder uhmsetup.sh despliega tools/uhmiptables.sh como un placeholder: reenvío IPv4 y NAT, nada más. Ubuntu no hace ninguna de las dos cosas por defecto, y sin ellas los clientes LAN obtienen lease pero no alcanzan nada. Sus reglas viven en dos cadenas dedicadas, UHM_NAT y UHM_FWD, vaciadas y reconstruidas en cada ejecución para que nunca se acumulen. UHM_FWD existe porque habilitar el reenvío en el kernel no basta si la política FORWARD es DROP. Nada fuera de esas dos cadenas se toca y ninguna política se cambia, así que un firewall gestionado por otra vía queda intacto. El placeholder no redirige al proxy, no filtra puertos, no ata MAC a IP y no construye ningún ipset. El control de acceso sigue aplicándose: UHM lo impone en la capa DHCP, mediante la clase de denegación blockdhcp que uhmleases.sh escribe en pydhcpd.conf. Para aplicación a nivel de firewall, copie tools/uhmiptables_example.txt sobre este archivo y adáptelo: respalde el placeholder como uhmiptables.sh.bak, copie el ejemplo sobre uhmiptables.sh y restaure el modo 750. Léalo completo antes de usarlo: asume un proxy squid en este mismo host, y sus reglas para las clases limited y hotspot le envían el tráfico. El archivo se despliega solo cuando falta y nunca se sobrescribe después, ya que pasa a ser propiedad del administrador una vez personalizado. La clasificación de clientes en gracia, autorizados y bloqueados la hace uhmd. A las MAC bloqueadas pydhcpd les niega el lease. El portal cautivo lo aplica el propio flag authorized por cliente de UniFi. Las tres cosas siguen funcionando con independencia de este archivo. Si el archivo falta, uhmreload.sh registra un warning y continúa en vez de tratarlo como fallo de reload. Ver uhmreload en la sección CORE para saber cómo se maneja el fallo de este script y el de uhmleases.sh.
|
⚠️ WARNING: Keep large blocklists out of this script. Useiptablesfor this project's own purposes, such as allowing or denying traffic by MAC/IP and port, as well as the captive-portal redirects. Do not useiptablesto manage large lists of domains, IP addresses, reputation or content. For that kind of filtering, specialized tools are recommended, such asFail2ban,Unbound,Squid,Suricata, among others. Bear in mind thatuhmiptables.shruns in full on every reload, and every reload stops and startspydhcpd. Large lists can slow those cycles down and increase the risk of collisions while they run.
⚠️ WARNING: Mantenga las listas de bloqueo grandes fuera de este script. Useiptablespara las funciones propias de este proyecto, como permitir o denegar tráfico por MAC/IP y puerto, así como las redirecciones del portal cautivo. No utiliceiptablespara gestionar grandes listas de dominios, direcciones IP, reputación o contenido. Para este tipo de filtrado se recomienda utilizar herramientas especializadas, comoFail2ban,Unbound,Squid,Suricata, entre otras. Tenga en cuenta queuhmiptables.shse ejecuta completamente en cada reload, y cada reload detiene y vuelve a iniciarpydhcpd. La presencia de listas grandes puede ralentizar estos ciclos y aumentar el riesgo de colisiones durante su ejecución.
Required UniFi ports (hardcoded in uhmiptables.sh):
| Port | Proto | Direction | Purpose | Propósito |
|---|---|---|---|---|
| 8080 | TCP | LAN → controller | AP-to-controller communication | Comunicación AP-controlador |
| 8880 | TCP | LAN → controller | Captive portal HTTP | Portal cautivo HTTP |
| 8881 | TCP | LAN → controller | Captive portal HTTP alternate | Portal cautivo HTTP alternativo |
| 8882 | TCP | LAN → controller | Captive portal HTTP alternate | Portal cautivo HTTP alternativo |
| 8843 | — | not opened | Captive portal HTTPS -- not used: UHM only serves the captive portal over plain HTTP, never HTTPS (see UNIFI PRE-CONFIGURATION above) |
No usado: UHM sirve el portal cautivo solo por HTTP plano, nunca HTTPS (ver UNIFI PRE-CONFIGURATION arriba) |
| 6789 | TCP | LAN → controller | UniFi speed test / throughput measurement | Prueba de velocidad UniFi / medición de throughput |
| 10001 | UDP | LAN ↔ APs | Device discovery | Descubrimiento de dispositivos |
| 3478 | UDP | LAN → WAN | STUN for APs behind NAT | STUN para APs detrás de NAT |
| 123 | UDP | LAN → WAN | NTP time sync | Sincronización NTP |
For the full list of UniFi required ports see: help.ui.com/hc/en-us/articles/218506997
Para la lista completa de puertos requeridos por UniFi, consulte: help.ui.com/hc/en-us/articles/218506997
core/ holds the reload mechanism itself. See Scope. uhmd.sh and uhmd.service run the daemon. Whenever an ACL changes, the daemon calls uhmreload.sh, which runs uhmleases.sh to synchronize the ACLs and DHCP leases. tools/, the next section, holds independent and optional utilities. uhmiptables.sh is the only exception living under tools/: it is needed to enforce the firewall, but its absence does not prevent uhmd from starting or from classifying clients correctly. See Failure handling under uhmreload below.
|
core/ contiene los componentes principales de UHM. Consulta la sección Scope. uhmd.sh y uhmd.service ejecutan el daemon. Cuando cambia una ACL, el daemon llama a uhmreload.sh, que ejecuta uhmleases.sh para sincronizar las ACL y las concesiones DHCP. tools/, la sección siguiente, contiene utilidades independientes y opcionales. uhmiptables.sh es la única excepción que vive bajo tools/: es necesario para aplicar el firewall, pero su ausencia no impide que uhmd arranque ni que clasifique clientes correctamente. Ver Failure handling bajo uhmreload más abajo.
|
uhmd.sh is UHM's main background service. Every POLL_INTERVAL seconds (20 by default), it queries the UniFi controller and coordinates updates to the ACL files. See Daemon Cycle above for the full 11-step breakdown.
Installed at /etc/uhm/core/uhmd.sh.
|
uhmd.sh es el proceso principal de UHM y se ejecuta como servicio en segundo plano. Cada POLL_INTERVAL segundos (20 por defecto), consulta el controlador UniFi y coordina la actualización de las ACL. Ver Daemon Cycle arriba para el detalle completo de los 11 pasos.
Instalado en /etc/uhm/core/uhmd.sh.
|
After a server reboot, the login endpoint may respond before UniFi's data APIs (stat/voucher, stat/guest, and stat/sta) are ready. A successful login therefore does not mean the controller is fully operational; the log records both events separately:
|
Después de reiniciar el servidor, el inicio de sesión puede responder antes de que las API de datos de UniFi (stat/voucher, stat/guest y stat/sta) estén listas. Por eso, iniciar sesión correctamente no significa que el controlador ya esté completamente operativo; el registro muestra ambos momentos por separado:
|
2026-07-12 21:41:10 INFO: UniFi login failed (HTTP 000) in grace -- skip
2026-07-12 21:41:20 INFO: UniFi login failed (HTTP 000) in grace -- skip
2026-07-12 21:41:30 INFO: UniFi login failed (HTTP 000) in grace -- skip
2026-07-12 21:41:50 INFO: UniFi login OK
2026-07-12 21:41:51 INFO: Could not load vouchers (rc=empty) -- skip
2026-07-12 21:41:56 INFO: sessions step, stat/guest unavailable -- skip
2026-07-12 21:41:56 INFO: revoke step, stat/sta unavailable -- skip
2026-07-12 21:42:11 INFO: Could not load vouchers (rc=empty) -- skip
2026-07-12 21:42:16 INFO: sessions step, stat/guest unavailable -- skip
2026-07-12 21:42:16 INFO: revoke step, stat/sta unavailable -- skip
2026-07-12 21:42:31 INFO: UniFi backend ready (voucher/guest/sta OK)
Both parts are expected and self-resolving. The login retries are
uhmd.shwaiting outSTARTUP_GRACE_SECONDSwhile UniFi OS itself is still coming up. The couple of data-endpoint failures right after a successful login happen because UniFi OS brings its auth endpoint up slightly before the rest of its API is ready to serve — a few seconds of lag, not a real failure.UniFi backend readylogs exactly once, on the transition from any ofstat/voucher/stat/guest/stat/stafailing to all three succeeding together — the single line to watch for "the daemon is now fully operational" instead of inferring it from the absence of further warnings.Ambas situaciones son temporales y suelen resolverse por sí solas. Los reintentos de inicio de sesión ocurren mientras
uhmd.shespera a que termineSTARTUP_GRACE_SECONDSy UniFi OS completa su arranque. Los fallos de las API de datos justo después de iniciar sesión ocurren porque UniFi OS habilita el servicio de autenticación antes que el resto de la API. Suelen durar unos segundos.UniFi backend readyse registra exactamente una vez, en la transición de cualquiera destat/voucher/stat/guest/stat/stafallando a los tres respondiendo juntos — la línea que confirma que el daemon ya está completamente operativo; no hace falta inferirlo por la ausencia de advertencias.
mac-*.txt files are entirely optional. uhmsetup.sh only creates the empty /etc/acl/mac directory; it never creates any mac-*.txt file itself. uhmleases.sh does create mac-limited.txt and mac-unlimited.txt (empty) on its first run if they're missing, and leaving both files empty is a fully supported configuration: with no managed MACs, every client goes through the normal guest flow (grace → voucher → captive portal), with no exceptions. Nothing in uhmd.sh or uhmleases.sh requires a non-empty mac-*.txt to function — every place that reads them (a glob with nullglob, or a fixed path already guaranteed to exist) degrades cleanly to "nothing is managed" when they're empty or absent.
|
Los archivos mac-*.txt son totalmente opcionales. uhmsetup.sh solo crea el directorio vacío /etc/acl/mac; nunca crea ningún archivo mac-*.txt por sí mismo. uhmleases.sh sí crea mac-limited.txt y mac-unlimited.txt (vacíos) en su primera ejecución si faltan, pero dejar ambos archivos vacíos es una configuración admitida: sin MACs gestionadas, todo cliente pasa por el flujo normal de invitados (gracia → voucher → portal cautivo), sin excepciones. UHM funciona aunque esos archivos estén vacíos o no existan: en ese caso, trata todos los clientes como no gestionados.
|
Recommendation: infrastructure equipment that gets its DHCP lease from the same pydhcpd instance as the guest network (APs, switches, and similar communications gear on the same subnet) should be listed in mac-unlimited.txt. Without an entry, such a device is indistinguishable from any unknown guest client: it enters uhm-grace.txt on first lease, and once BLOCKDHCP_GRACE_SECONDS elapses without a voucher — which infrastructure gear has no way to redeem, since it never opens the captive portal itself — uhmleases.sh moves it to blockdhcp.txt, and pydhcpd denies it any further lease. That is a verified mechanism, not a guess; whether losing DHCP renewal actually degrades that specific device (reboot loop, lost management access, etc.) depends on the device itself and is outside what this project's code can determine — the safe default is simply not to let infrastructure gear go through the same unknown-client path guests do.
|
Recomendación: añade a mac-unlimited.txt los puntos de acceso, switches y demás equipos de red que reciben DHCP de esta instancia de pydhcpd. Si no los incluyes, UHM puede tratarlos como clientes invitados y bloquearlos al terminar el período de gracia, ya que no pueden canjear un voucher.
|
The independent watcher in Daemon Cycle detects edits to any mac-*.txt file by comparing a combined MD5 fingerprint of the whole set across cycles. It does not determine which MAC or field changed. It marks the change in the cycle when detected; the reload runs on the next cycle:
|
El supervisor independiente descrito en Daemon Cycle detecta cambios en cualquier archivo mac-*.txt comparando una huella MD5 combinada del conjunto entre ciclos. No identifica qué MAC o campo cambió. Marca el cambio cuando lo detecta y ejecuta la recarga en el ciclo siguiente:
|
2026-07-23 14:13:45 INFO: mac-*.txt changed, reload next cycle
2026-07-23 14:14:05 INFO: mac-*.txt changed, reloading now
2026-07-23 14:14:05 INFO: invoking /etc/uhm/core/uhmreload.sh
Whatever the edit actually was (block/reactivate/add/remove/IP change), uhmleases.sh is what interprets it on that reload: an active (a;) line gets a fixed-address DHCP entry; a commented (#a;) line joins the same blockdhcp deny class as blockdhcp.txt, so pydhcpd denies it a lease outright.
|
Sea cual sea la edición real (bloqueo/reactivación/alta/baja/cambio de IP), uhmleases.sh es quien la interpreta en ese reload: una línea activa (a;) recibe una entrada DHCP de dirección fija; una línea comentada (#a;) entra en la misma clase de denegación blockdhcp que blockdhcp.txt, así que pydhcpd le niega el lease directamente.
|
Systemd unit for uhmd.sh. Restart=always with RestartSec=10 restarts the daemon on any crash; StartLimitIntervalSec=300 / StartLimitBurst=10 (in [Unit]) cap it at 10 restarts per 5 minutes before systemd marks it start-limit-hit and stops trying — a general crash-loop guard, not specific to any one failure mode. After=network.target pydhcpd.service / Wants=pydhcpd.service order startup after the DHCP backend, though uhmd.sh still tolerates pydhcpd coming up late via its own startup grace (see Daemon Cycle).
Installed at /etc/systemd/system/uhmd.service, deployed from the repo's service/uhmd.service.
Note — sandboxing: PrivateTmp=yes, ProtectHome=read-only, ProtectControlGroups=yes, ProtectClock=yes, ProtectHostname=yes, ProtectKernelLogs=yes, LockPersonality=yes, RestrictRealtime=yes and RestrictSUIDSGID=yes are applied — none of them intersect any path or syscall this daemon or its reload chain actually uses (PrivateTmp gives uhmreload.sh's trace files and uhmleases.sh's mktemp calls an isolated /tmp, with no downside since nothing outside the reload chain needs to see them). One more common hardening directive is intentionally not set, because it would break real functionality: ProtectSystem=strict would make /etc read-only, but uhmleases.sh rewrites /etc/pydhcp/core/pydhcpd.conf and pydhcpd.leases on every reload, and the admin-supplied uhmiptables.sh is arbitrary code that may need to write anywhere on the system (persistent ipset/iptables rule files, etc.) — a static ReadWritePaths allowlist can't be correct in general for a script the admin fully controls.
|
Unit systemd para uhmd.sh. Restart=always con RestartSec=10 reinicia el daemon ante cualquier caída; StartLimitIntervalSec=300 / StartLimitBurst=10 (en [Unit]) lo limitan a 10 reinicios cada 5 minutos antes de que systemd lo marque start-limit-hit y deje de intentarlo — una protección general contra crash-loops, no específica de un solo modo de fallo. After=network.target pydhcpd.service / Wants=pydhcpd.service ordenan el arranque después del servidor DHCP, aunque uhmd.sh igual tolera que pydhcpd arranque tarde gracias a su propio período de gracia al inicio (ver Daemon Cycle).
Instalado en /etc/systemd/system/uhmd.service, desplegado desde service/uhmd.service del repositorio.
Nota — sandboxing: se aplican PrivateTmp=yes, ProtectHome=read-only, ProtectControlGroups=yes, ProtectClock=yes, ProtectHostname=yes, ProtectKernelLogs=yes, LockPersonality=yes, RestrictRealtime=yes y RestrictSUIDSGID=yes — ninguna interseca con ninguna ruta o syscall que el daemon o su cadena de reload usen realmente (PrivateTmp le da a los trace files de uhmreload.sh y a los mktemp de uhmleases.sh un /tmp aislado, sin ninguna desventaja ya que nada fuera de la cadena de reload necesita verlos). Una directiva de hardening común se deja intencionalmente fuera, porque rompería funcionalidad real: ProtectSystem=strict dejaría /etc de solo lectura, pero uhmleases.sh reescribe /etc/pydhcp/core/pydhcpd.conf y pydhcpd.leases en cada reload, y el uhmiptables.sh que provee el administrador es código arbitrario que puede necesitar escribir en cualquier parte del sistema (archivos de persistencia de ipset/iptables, etc.) — una whitelist estática de ReadWritePaths no puede ser correcta en general para un script que el administrador controla por completo.
|
uhmreload.sh synchronizes the DHCP leases and firewall rules after an ACL change. uhmd also runs it periodically according to RELOAD_SAFETY_INTERVAL_SECONDS (one hour by default), even when no ACL has changed. This lets UHM promote expired grace entries and rebuild the firewall on idle networks. You can run the script manually for troubleshooting, but only while uhmd.service is active. It runs uhmleases.sh first and then uhmiptables.sh; the two scripts handle failures differently, as described below.
This asymmetry reflects what each script actually is: uhmleases.sh is the core ACL/lease reconciliation step — nothing downstream can be trusted without it. uhmiptables.sh only enforces at the firewall level, and ships as a working placeholder (see Firewall Rules) that a normal install always has in place. Only its absence is tolerated, with a warning; a genuine execution failure of uhmiptables.sh still aborts.
Installed at /etc/uhm/core/uhmreload.sh.
|
uhmreload.sh sincroniza las concesiones DHCP y las reglas del firewall después de un cambio en las ACL. uhmd también lo ejecuta periódicamente según RELOAD_SAFETY_INTERVAL_SECONDS (una hora por defecto), aunque las ACL no hayan cambiado. Así, UHM puede pasar a bloqueo las entradas de gracia vencidas y reconstruir el firewall en redes sin actividad. Puedes ejecutar el script manualmente para diagnosticar problemas, pero solo mientras uhmd.service esté activo. Primero ejecuta uhmleases.sh y luego uhmiptables.sh; cada script gestiona los errores de forma distinta, como se explica abajo.
Esta asimetría refleja lo que cada script realmente es: uhmleases.sh es el paso central de reconciliación de ACLs/leases — nada aguas abajo es confiable sin él. uhmiptables.sh solo aplica a nivel de firewall, y se despliega como un placeholder funcional (ver Firewall Rules) que toda instalación normal tiene en su sitio. Solo su ausencia se tolera, con una advertencia; un fallo real de ejecución de uhmiptables.sh sigue abortando.
Instalado en /etc/uhm/core/uhmreload.sh.
|
Two separate triggers invoke uhmreload.sh, each logged differently so the reason is clear from the log alone: / Dos disparadores distintos invocan uhmreload.sh, cada uno con un log diferente para que la razón sea clara solo con leerlo:
| Trigger | Log line | Description | Descripción |
|---|---|---|---|
| Cycle | 2026-07-23 22:01:31 INFO: invoking /etc/uhm/core/uhmreload.sh |
The normal case: an ACL file actually changed (or RELOAD_SAFETY_INTERVAL_SECONDS elapsed), detected in check_and_reload_if_changed() every POLL_INTERVAL |
El caso normal: una ACL realmente cambió (o venció RELOAD_SAFETY_INTERVAL_SECONDS), detectado en check_and_reload_if_changed() en cada POLL_INTERVAL |
| Startup | 2026-08-11 07:53:05 INFO: startup, invoking uhmreload |
On every uhmd.sh start, regardless of ACL state: iptables/ipset rules don't survive a reboot even if the ACL files themselves didn't change, so this one fires unconditionally instead of waiting for a diff |
En cada inicio de uhmd.sh, sin importar el estado de las ACLs: las reglas de iptables/ipset no sobreviven un reboot aunque los archivos ACL no hayan cambiado, así que esta se dispara sin condición en vez de esperar un diff |
| Script | Condition | Description | Descripción |
|---|---|---|---|
uhmleases.sh |
Missing | Abort reload (ERROR + exit 1) |
Aborta el reload (ERROR + exit 1) |
uhmleases.sh |
Fails during execution | Abort reload (ERROR + exit 1) |
Aborta el reload (ERROR + exit 1) |
uhmiptables.sh |
Missing | Warn and continue -- reload still counts as done | Avisa y continúa -- el reload igual cuenta como hecho |
uhmiptables.sh |
Fails during execution | Abort reload (ERROR + exit 1) |
Aborta el reload (ERROR + exit 1) |
uhmd.sh waits for uhmreload.sh with no time limit of its own. uhmreload.sh bounds each step individually instead: UHM_LEASES_TIMEOUT_SECONDS (default 120) and UHM_IPTABLES_TIMEOUT_SECONDS (default 60), both adjustable in uhm.env. A step that exceeds its limit is killed, its trace saved to /var/log/-failure.trace, and the reload aborts the same way as any other failure. This is a single fixed-name file per step (uhmleases-failure.trace, uhmiptables-failure.trace), overwritten on every new failure of that step -- not one file per attempt, so it never accumulates. A successful run leaves the previous trace (if any) untouched; the file only reflects the most recent failure.
|
uhmd.sh espera a uhmreload.sh sin ningún límite de tiempo propio. uhmreload.sh acota cada paso por separado: UHM_LEASES_TIMEOUT_SECONDS (default 120) y UHM_IPTABLES_TIMEOUT_SECONDS (default 60), ambos ajustables en uhm.env. Un paso que excede su límite se mata, su trace se guarda en /var/log/-failure.trace, y el reload aborta igual que cualquier otro fallo. Es un único archivo de nombre fijo por paso (uhmleases-failure.trace, uhmiptables-failure.trace), sobrescrito en cada nueva falla de ese paso — no un archivo por intento, así que nunca se acumula. Una corrida exitosa deja el trace anterior (si existe) intacto; el archivo solo refleja la falla más reciente.
|
uhmleases.sh is a reimplementation of the pyleases.sh shipped by default with pydhcp, with built-in UniFi Hotspot integration. The original version manages DHCP leases and ACLs but has no awareness of the UniFi captive portal. This version adds the UniFi Hotspot Integration module: uhmleases reads /etc/uhm/acl/uhm-auth.txt and /etc/uhm/acl/uhm-grace.txt as authoritative classification lists during lease processing, applies a grace period for unseen MACs (BLOCKDHCP_GRACE_SECONDS, default 24h), and synchronizes hotspot-related ACL entries.
The script runs from /etc/uhm/core/uhmleases.sh and detects the existence of /etc/pydhcp (required). Configuration is read exclusively from /etc/uhm/uhm.env (generated and managed by uhmsetup.sh). To reconfigure, edit uhm.env directly or re-run uhmsetup.sh.
Two locks, two purposes. /var/lock/uhmleases.lock only prevents a second copy of this same script: if it is already taken, the run aborts with an ERROR. /var/lock/uhmd-cycle.lock is the mechanism lock, shared with uhmd.sh and with the panel's ACL save. It is acquired unconditionally, whoever invoked the script — the daemon cycle, uhmreload.sh, or a manual run — because the guard belongs to the script that writes, not to its caller. The wait is bounded to 10 seconds; if the lock is still held, the run logs INFO: mechanism busy -- skip and exits 0, with no changes. Once taken, it is held for the rest of the execution, covering the whole stop/modify/start window of pydhcpd, and released on exit.
|
uhmleases.sh es una reimplementación del pyleases.sh que viene por defecto con pydhcp, con integración UniFi Hotspot incorporada. La versión original gestiona leases DHCP y ACLs pero no sabe nada del portal cautivo de UniFi. Esta versión añade el módulo UniFi Hotspot Integration: uhmleases lee /etc/uhm/acl/uhm-auth.txt y /etc/uhm/acl/uhm-grace.txt como listas autoritativas de clasificación durante el procesamiento de leases, aplica un período de gracia para MACs nuevas (BLOCKDHCP_GRACE_SECONDS, default 24h), y sincroniza entradas ACL relacionadas con el hotspot.
El script se ejecuta desde /etc/uhm/core/uhmleases.sh y detecta la existencia de /etc/pydhcp (requerido). La configuración se lee exclusivamente desde /etc/uhm/uhm.env (generado y gestionado por uhmsetup.sh). Para reconfigurar, edite uhm.env directamente o vuelva a correr uhmsetup.sh.
Dos locks, dos propósitos. /var/lock/uhmleases.lock solo impide que se ejecute una segunda copia del mismo script: si ya está tomado, la corrida aborta con un ERROR. /var/lock/uhmd-cycle.lock es el lock del mecanismo, compartido con uhmd.sh y con el guardado de ACL del panel. Se toma siempre, sin importar quién invoque el script — el ciclo del daemon, uhmreload.sh, o una corrida manual — porque la protección le corresponde al script que escribe, no a quien lo llama. La espera está acotada a 10 segundos; si el lock sigue tomado, la corrida registra INFO: mechanism busy -- skip y sale con 0, sin hacer cambios. Una vez tomado, se conserva durante el resto de la ejecución, cubriendo toda la ventana de detención, modificación y arranque de pydhcpd, y se libera al salir.
|
⚠️ WARNING:uhmleases.shandpyleases.shboth fully rebuild the same/etc/pydhcp/core/pydhcpd.conffrom ACL sources on every run. They are mutually exclusive on the same installation — running both (e.g. one from cron, the other viauhmreload.sh) makes each overwrite the other's rebuild, silently discarding whichever directives the other one doesn't know about (the UniFi Hotspot ACL entries fromuhmleases.sh, or any change made throughpyleases.sh). If you installUHM, useuhmleases.shexclusively and do not runpyleases.shon the same host. Classes and pools: thepydhcpddaemon supports severalpool { }blocks and any number ofclass/subclassdeclarations, exactly asisc-dhcp-serverdoes.uhmleases.sh, by design, only ever writes what this project documents: one pool withdeny members of "blockdhcp";, plus thefixed-addressreservations from the ACL lists. Any extra class or pool added by hand topydhcpd.confis discarded on the next run. This is not a hard limit:uhmleases.shis a plain shell script, so anyone who needs extra classes or pools can edit the block that writespydhcpd.confand emit them there — the daemon will honour whatever the file ends up containing. Keep your own copy of any such change:uhmsetup.sh --updatereplaces the script with the shipped version, and althoughuhmbk.shsaves the previous one inside/etc/bak/uhm/uhmbk_<YYYYMMDD_HHMM>.zip, the edit has to be reapplied by hand after every update.
⚠️ WARNING:uhmleases.shypyleases.shreconstruyen completamente el mismo/etc/pydhcp/core/pydhcpd.confa partir de fuentes ACL en cada ejecución. Son mutuamente excluyentes en la misma instalación — correr ambos (por ejemplo uno desde cron y el otro víauhmreload.sh) hace que cada uno sobrescriba la reconstrucción del otro, descartando en silencio las directivas que el otro no conoce (las entradas ACL de UniFi Hotspot deuhmleases.sh, o cualquier cambio hecho mediantepyleases.sh). Si instalaUHM, use exclusivamenteuhmleases.shy no ejecutepyleases.shen el mismo host. Clases y pools: el demoniopydhcpdsoporta varios bloquespool { }y cualquier cantidad de declaracionesclass/subclass, igual queisc-dhcp-server.uhmleases.sh, por diseño, solo escribe lo que este proyecto documenta: un pool condeny members of "blockdhcp";, más las reservasfixed-addressde las listas ACL. Cualquier clase o pool agregado a mano apydhcpd.confse descarta en la siguiente ejecución. No es una camisa de fuerza:uhmleases.shes un script de shell corriente, así que quien necesite clases o pools adicionales puede editar el bloque que escribepydhcpd.confy emitirlos ahí — el demonio va a respetar lo que el archivo termine conteniendo. Guarde su propia copia de ese cambio:uhmsetup.sh --updatereemplaza el script por la versión del repositorio y, aunqueuhmbk.shrespalda el anterior dentro de/etc/bak/uhm/uhmbk_<AAAAMMDD_HHMM>.zip, la edición hay que volver a aplicarla a mano tras cada actualización.
ACL sources consumed by uhmleases:
| Path | Role | Rol |
|---|---|---|
/etc/acl/mac/mac-limited.txt |
Authorized — forced through Squid | Autorizados — forzados por Squid |
/etc/acl/mac/mac-unlimited.txt |
Authorized — bypass restrictions | Autorizados — sin restricciones |
/etc/pydhcp/acl/blockdhcp.txt |
Blocked clients | Clientes bloqueados |
/etc/uhm/acl/uhm-grace.txt |
Grace-period clients | Período de gracia |
/etc/uhm/acl/uhm-auth.txt |
Hotspot — voucher active | Hotspot — voucher activo |
Entry format:
Standard : a;MAC;IP;HOSTNAME;
Hotspot : a;MAC;IP;HOSTNAME;END_TIME_EPOCH;
Grace : a;MAC;IP;HOSTNAME;FIRST_SEEN_EPOCH;
| Notation | Meaning | Significado |
|---|---|---|
Leading a |
Marks a well-formed, active entry -- any other leading character is treated as malformed (see ACL priority order). There is no opposite value (no i/d/etc.) |
Marca una entrada activa y bien formada -- cualquier otro carácter inicial se trata como malformado (ver ACL priority order). No existe un valor opuesto (no hay i/d/etc.) |
Leading # (comment out) |
Deactivates an entry -- comment out the whole line (e.g. #a;MAC;IP;HOSTNAME;) instead of changing the a itself. Only valid in mac-*.txt and uhm-auth.txt, the only two lists that ever produce a fixed-address host { } block in pydhcpd.conf; a commented entry there loses its fixed address and joins the same blockdhcp deny class as blockdhcp.txt. In uhm-auth.txt, this only affects DHCP-level treatment -- it does NOT exempt the entry from expiring by END_TIME_EPOCH (see clean_expired_macs); mac-*.txt has no such field, so there's nothing to expire there |
Desactiva una entrada -- comenta la línea completa (p.ej. #a;MAC;IP;HOSTNAME;) en vez de cambiar la a misma. Solo es válido en mac-*.txt y uhm-auth.txt, las únicas dos listas que producen un bloque host { } de dirección fija en pydhcpd.conf; una entrada comentada ahí pierde su dirección fija y entra en la misma clase de denegación blockdhcp que blockdhcp.txt. En uhm-auth.txt, esto solo afecta el tratamiento a nivel DHCP -- NO exime a la entrada de vencer por END_TIME_EPOCH (ver clean_expired_macs); mac-*.txt no tiene ese campo, así que ahí no hay nada que vencer |
# in blockdhcp.txt, uhm-grace.txt, lease removal queue |
Not supported -- these lists have no active/inactive concept (blockdhcp.txt is already a terminal deny state, uhm-grace.txt is purely temporary/self-expiring, and the lease removal queue is a working list with no a;/#a; syntax at all). A #-prefixed line in any of them is treated as malformed and dropped from the file, same as any other invalid line |
No soportado -- estas listas no tienen concepto de activo/inactivo (blockdhcp.txt ya es un estado terminal de denegación, uhm-grace.txt es puramente temporal y autoexpira, y la cola de remoción de leases es una lista de trabajo sin sintaxis a;/#a; en absoluto). Una línea con # en cualquiera de ellas se trata como malformada y se elimina del archivo, igual que cualquier otra línea inválida |
⚠️ WARNING -- hand-editing an authorization list.mac-*.txtanduhm-auth.txtare the two lists that grant access, and they are the only two where a malformed line aborts the reload instead of being dropped. That is deliberate: silently deleting a line there would revoke a device's access — or a paying guest's — with nothing on record but its disappearance. A typo while commenting or uncommenting an entry stopsuhmleases.shwith anERRORnaming the file and the line number,pydhcpd.confis not rebuilt, and the firewall keeps the previous state until you fix it. Check the log after editing either file by hand:tail -f /var/log/uhm.log. The remaining lists (blockdhcp.txt,uhm-grace.txt, the lease removal queue) are derived and transient — they authorize nothing, so a bad line there is dropped and the run continues.
⚠️ ADVERTENCIA -- editar a mano una lista de autorización.mac-*.txtyuhm-auth.txtson las dos listas que conceden acceso, y las dos únicas donde una línea malformada aborta el reload en vez de eliminarse. Es deliberado: borrar en silencio una línea ahí le quitaría el acceso a un dispositivo — o a un invitado que pagó su voucher — sin más constancia que su desaparición. Un error de tecleo al comentar o descomentar una entrada detieneuhmleases.shcon unERRORque nombra el archivo y el número de línea,pydhcpd.confno se reconstruye, y el firewall conserva el estado anterior hasta que usted lo corrija. Revise el log después de editar a mano cualquiera de esos dos archivos:tail -f /var/log/uhm.log. Las demás listas (blockdhcp.txt,uhm-grace.txt, la cola de remoción de leases) son derivadas y transitorias — no autorizan nada, así que una línea mala ahí se elimina y la corrida sigue.
Both are covered per list in ACL priority order -- which list aborts the reload and which one drops the line and continues, and which side loses a duplicate. Not repeated here. Apart from that check, uhmd.sh makes its own pass every cycle, far more often than a reload:
|
Ambos están cubiertos por lista en ACL priority order -- qué lista aborta el reload y cuál descarta la línea y continúa, y qué lado pierde un duplicado. No se repite aquí. Aparte de esa verificación, uhmd.sh hace su propia pasada en cada ciclo, mucho más frecuente que un reload:
|
| File | Description | Descripción |
|---|---|---|
blockdhcp.txt |
The dedup step recovers a line if MAC/IP/hostname can still be parsed out validly (e.g. a missing trailing ;); otherwise it discards it rather than writing it back broken. |
El paso dedup recupera la línea si aún se pueden extraer MAC, IP y hostname válidos (ej. falta el ; final); si no, la descarta en vez de reescribirla rota. |
uhm-auth.txt |
The expire step releases a line with a malformed END_TIME_EPOCH like an expired one. With no readable expiry the entry cannot be sustained, and keeping it would hold a hotspot IP forever if the client never reassociates. It repairs itself: a client whose voucher is still valid is promoted again next cycle, with an END_TIME_EPOCH from UniFi. |
El paso expire libera una línea con END_TIME_EPOCH malformado igual que una vencida. Sin vencimiento legible la entrada no se puede sostener, y conservarla retendría una IP del hotspot para siempre si el cliente no vuelve a asociarse. Se autorrepara: un cliente cuyo voucher sigue vigente vuelve a promoverse en el ciclo siguiente, con un END_TIME_EPOCH que viene de UniFi. |
mac-*.txt, uhm-queue.txt |
Never rewritten by uhmd.sh. |
uhmd.sh nunca las reescribe. |
| Aspect | Description | Descripción |
|---|---|---|
| Reason for stop/start | Stopping guarantees exclusive access to the leases file while it's rewritten, avoiding a race with a lease the daemon might be persisting at that instant | Detenerlo garantiza acceso exclusivo al archivo de leases mientras se reescribe, evitando una carrera con un lease que el daemon pudiera estar persistiendo en ese instante |
| Trade-off | Brief DHCP downtime on every ACL change, accepted for write safety | Breve corte de DHCP en cada cambio de ACL, aceptado a cambio de seguridad en la escritura |
Install (already covered in the Install section above):
# uhmleases.sh is deployed automatically by uhmsetup.sh to /etc/uhm/core/
# Configuration is read from /etc/uhm/uhm.env (managed by uhmsetup.sh)
# No manual setup required — run uhmsetup.sh to configure everythingConfiguration variables (in uhm.env):
| Variable | Default | Description | Descripción |
|---|---|---|---|
SERVER_IP |
(from pydhcp.env) | DHCP server IP address | Dirección IP del servidor DHCP |
SERV_SUBNET |
(from pydhcp.env) | Network subnet | Subred de red |
SERV_BROADCAST |
(from pydhcp.env) | Broadcast address | Dirección de broadcast |
SERV_MASK |
(from pydhcp.env) | Netmask | Máscara de red |
SERV_INI_RANGE_BLOCK |
(from pydhcp.env) | Start of block pool IP range | Inicio del rango de IP del pool de bloqueo |
SERV_END_RANGE_BLOCK |
(from pydhcp.env) | End of block pool IP range | Fin del rango de IP del pool de bloqueo |
SERV_DNS |
(from pydhcp.env) | DNS servers (comma-separated) | Servidores DNS (separados por coma) |
ACL_PATH |
(from pydhcp.env) | Base path for ACL directories | Ruta base para los directorios ACL |
ACL_MAC_PATH |
(from pydhcp.env) | MAC-based ACL directory | Directorio ACL basado en MAC |
ACL_DHCP_PATH |
(from pydhcp.env) | DHCP ACL directory | Directorio ACL de DHCP |
UHM_PATH |
/etc/uhm | Hotspot working directory | Directorio de trabajo del hotspot |
ACL_MAC_LIMITED |
(from pydhcp.env) | Proxy-forced clients | Clientes forzados por proxy |
ACL_MAC_UNLIMITED |
(from pydhcp.env) | Unrestricted clients | Clientes sin restricciones |
UHM_MACAUTH |
/etc/uhm/acl/uhm-auth.txt | Hotspot authorized -- UHM's own | Autorizados del hotspot -- propia de UHM |
ACL_BLOCK_FILE |
(from pydhcp.env) | Blocked clients | Clientes bloqueados |
UHM_GRACE |
/etc/uhm/acl/uhm-grace.txt | Grace period clients -- UHM's own | Clientes en período de gracia -- propia de UHM |
BLOCKDHCP_GRACE_SECONDS |
86400 | Grace period duration (seconds, 24h). On expiry the MAC moves to blockdhcp.txt on the next reload, not at the instant the timer runs out |
Duración del período de gracia (segundos, 24h). Al expirar, la MAC pasa a blockdhcp.txt en el siguiente reload, no en el instante en que vence el contador |
| (derived) | AUTHORIZED_LEASE_TIME / 60 |
authorize-guest duration in minutes for mac-*.txt MACs UniFi reports unauthorized -- taken from pydhcp's own lease time, not a separate UHM value |
Duración de authorize-guest en minutos para MACs de mac-*.txt que UniFi reporta sin autorizar -- tomada del propio lease time de pydhcp, no es un valor aparte de UHM |
CLEANUP_INTERVAL |
(from pydhcp.env) | Cleanup frequency and pool lease time (seconds) | Frecuencia de limpieza y tiempo de lease del pool (segundos) |
AUTHORIZED_LEASE_TIME |
(from pydhcp.env) | Lease duration for authorized clients (30 days) | Duración del lease para clientes autorizados (30 días) |
QUARANTINE_DURATION |
(from pydhcp.env) | Seconds an IP is held out of the pool after a DHCPDECLINE or a ping-check conflict, written into pydhcpd.conf as abandon-lease-time (default 60) |
Segundos que una IP se aparta del pool tras un DHCPDECLINE o un conflicto de ping-check, escrito en pydhcpd.conf como abandon-lease-time (default 60) |
WPAD_ENABLED |
(from pydhcp.env) | Enable WPAD/PAC via DHCP option 252. Only takes effect if the PAC URL actually answers HTTP 200 (see WPAD/PAC in Operational Details) |
Habilitar WPAD/PAC vía la opción DHCP 252. Solo tiene efecto si la URL del PAC responde realmente HTTP 200 (ver WPAD/PAC en Operational Details) |
WPAD_PORT |
(from pydhcp.env) | TCP port of the Apache VirtualHost serving wpad.pac (default 18100). Keep it in sync with the PAC port hardcoded in uhmiptables.sh |
Puerto TCP del VirtualHost de Apache que sirve wpad.pac (default 18100). Manténgalo sincronizado con el puerto del PAC que uhmiptables.sh lleva fijo |
PING_CHECK_ENABLED |
(from pydhcp.env) | Ping IP before OFFER to detect conflicts. Set to false in environments with strict ICMP firewall rules |
Hacer ping a la IP antes del OFFER para detectar conflictos. Configurar en false en entornos con reglas de firewall ICMP estrictas |
PING_TIMEOUT_SECONDS |
(from pydhcp.env) | Seconds to wait for the ICMP reply before giving up and sending the OFFER, written into pydhcpd.conf as ping-timeout (default 1) |
Segundos a esperar la respuesta ICMP antes de desistir y enviar el OFFER, escrito en pydhcpd.conf como ping-timeout (default 1) |
Variables marked (from pydhcp.env) live in
/etc/pydhcp/pydhcp.envand are read from there at runtime -- they are never copied intouhm.env, so a change in that file reaches uhm without a re-install.uhmsetup.shnever asks for them. Most other variables have sensible defaults and can be modified directly inuhm.env, but the ACL paths, the lease file andBLOCKDHCP_GRACE_SECONDShave none:uhmtool.shaborts if any of them is missing.Las variables marcadas como (from pydhcp.env) viven en
/etc/pydhcp/pydhcp.envy se leen de ahí en cada ejecución -- nunca se copian auhm.env, así que un cambio en ese archivo llega a uhm sin reinstalar.uhmsetup.shnunca las pregunta. La mayoría de las demás tienen valores predeterminados sensatos y pueden modificarse directamente enuhm.env, pero las rutas de ACL, el archivo de concesiones yBLOCKDHCP_GRACE_SECONDSno los tienen:uhmtool.shaborta si falta alguna.
| Directive | Description | Descripción |
|---|---|---|
authoritative; |
Server sends NAK to clients with foreign leases | El servidor envía NAK a clientes con leases ajenos |
cleanup-interval N; |
How often (seconds) expired leases are removed from memory (controlled via CLEANUP_INTERVAL in uhm.env) |
Frecuencia (segundos) con que se eliminan leases expirados de memoria (controlado via CLEANUP_INTERVAL en uhm.env) |
abandon-lease-time N; |
Seconds an IP is held out of the pool after a DHCPDECLINE or ping-check conflict (controlled via QUARANTINE_DURATION in uhm.env) |
Segundos que una IP se aparta del pool tras un DHCPDECLINE o un conflicto de ping-check (controlado via QUARANTINE_DURATION en uhm.env) |
server-identifier IP; |
IP the server uses to identify itself in DHCP replies | IP con la que el servidor se identifica en las respuestas DHCP |
deny duplicates; |
Reject requests from a MAC that already holds a lease | Rechaza solicitudes de una MAC que ya tiene un lease |
deny declines; |
Ignore DHCPDECLINE messages | Ignora mensajes DHCPDECLINE |
ping-check true|false; |
Ping IP before OFFER to detect conflicts (controlled via PING_CHECK_ENABLED in uhm.env) |
Ping a la IP antes del OFFER para detectar conflictos (controlado via PING_CHECK_ENABLED en uhm.env) |
ping-timeout N; |
Seconds to wait for the ICMP reply before giving up and sending the OFFER (controlled via PING_TIMEOUT_SECONDS in uhm.env); default 1 |
Segundos a esperar la respuesta ICMP antes de desistir y enviar el OFFER (controlado via PING_TIMEOUT_SECONDS en uhm.env); default 1 |
option wpad ...; |
WPAD/PAC proxy auto-configuration (controlled via WPAD_ENABLED in uhm.env) |
Autoconfiguración de proxy WPAD/PAC (controlado via WPAD_ENABLED en uhm.env) |
subnet ... { pool { ... } } |
Subnet declaration with dynamic block pool | Declaración de subred con pool de bloqueo dinámico |
host NAME { hardware ethernet MAC; fixed-address IP; } |
Static host reservation from ACL files | Reserva estática de host desde archivos ACL |
class "blockdhcp" { ... } / subclass "blockdhcp" ... |
MAC-based DHCP block list | Lista de bloqueo DHCP por MAC |
min-lease-time, default-lease-time, max-lease-time |
Lease duration controls | Control de duración de leases |
option routers, option broadcast-address, option domain-name-servers |
Standard DHCP options | Opciones DHCP estándar |
uhmleases.sh fully rebuilds /etc/pydhcp/core/pydhcpd.conf on every run from its ACL files and uhm.env. Any manual edits to pydhcpd.conf — including custom lease times, pools, or directives — will be lost. If you manage pydhcpd.conf manually, do not use uhmleases.sh. |
uhmleases.sh reconstruye completamente /etc/pydhcp/core/pydhcpd.conf en cada ejecución a partir de sus archivos ACL y uhm.env. Cualquier edición manual a pydhcpd.conf — incluyendo lease times, pools o directivas personalizadas — se perderá. Si gestiona pydhcpd.conf manualmente, no utilice uhmleases.sh. |
Deactivating a managed MAC: commenting out a line in a mac-*.txt file (prefixing it with #) keeps it in place, IP included, but gives it the exact same treatment as a blockdhcp.txt entry — uhmleases.sh adds it to the "blockdhcp" DHCP class in pydhcpd.conf, so pydhcpd denies it a lease outright. It never physically enters blockdhcp.txt. |
Desactivar una MAC gestionada: comentar una línea en un archivo mac-*.txt (agregando # al inicio) la deja en su lugar, con su IP incluida, pero recibe exactamente el mismo tratamiento que una entrada de blockdhcp.txt — uhmleases.sh la agrega a la clase DHCP "blockdhcp" en pydhcpd.conf, así que pydhcpd le niega la lease directamente. Nunca entra físicamente a blockdhcp.txt. |
| Aspect | Description | Descripción |
|---|---|---|
| Scope | check_duplicate() is the single guard against duplicate ACL entries in uhmleases.sh — no other function detects or removes one. |
check_duplicate() es la única guarda contra entradas ACL duplicadas en uhmleases.sh — ninguna otra función detecta ni elimina una. |
| When it runs | Twice: right after normalization, to catch a hand-edited file before anything touches it, and again at the very end of the run, to catch a mistake made by the script's own processing in between. | Dos veces: justo después de la normalización, para atrapar un archivo editado a mano antes de que nada lo toque, y otra vez al final de la corrida, para atrapar un error del propio procesamiento del script. |
| Which list wins | See ACL priority order. | Ver ACL priority order. |
| Comparison | On the value alone — a commented (#a;) line counts the same as an active one. |
Solo por el valor — una línea comentada (#a;) cuenta igual que una activa. |
2026-09-25 13:43:38 INFO trace: uhmleases-failure.trace
2026-09-25 13:43:38 WARNING uhmreload failed (code 1), back off -- alert
2026-09-25 13:43:38 INFO uhmleases.sh failed (exit 1)
2026-09-25 13:43:38 ERROR trace: uhmleases-failure.trace -- abort
2026-09-25 13:43:38 ERROR uhmleases.sh failed (exit 1)
2026-09-25 13:43:38 INFO mac-*.txt duplicate entry
2026-09-25 13:43:38 INFO duplicate hostname foo1
2026-09-25 13:43:38 ERROR mac-*.txt duplicate entry -- abort
2026-09-25 13:43:38 ERROR duplicate hostname P-10
2026-09-25 13:43:38 INFO duplicate IP 192.168.0.166
2026-09-25 13:43:38 ERROR duplicate IP 192.168.0.166
2026-09-25 13:43:38 INFO duplicate MAC dc:62:79:d1:aa:bb
2026-09-25 13:43:38 ERROR duplicate MAC dc:62:79:d1:aa:bb
2026-08-25 10:00:00 INFO: dup MAC 'aa:bb:cc:dd:ee:01' removed from blockdhcp.txt
A separate guard, unrelated to duplicate detection and never merged into check_duplicate() — each function has a single purpose. Called alongside check_duplicate(), at the same two points (beginning and end of the script). Checks that no mac-*.txt IP falls inside a range reserved for something else. uhm.env only defines two IP ranges — UHM_INI_RANGE/UHM_END_RANGE (for uhm-auth.txt) and SERV_INI_RANGE_BLOCK/SERV_END_RANGE_BLOCK (the pydhcp pool used by uhm-grace.txt/blockdhcp.txt). mac-*.txt files are administrator-created and administrator-addressed — nothing in uhm.env reserves a range for them, so an IP picked by hand can land outside the LAN subnet, on the network/broadcast address, on SERVER_IP itself, or inside either of the other two ranges. This is always a misconfiguration, whether or not a guest currently holds that exact IP -- reported with a precise ERROR: line, then exit 1.
If neither guard finds a problem on the first pass, the script proceeds into is_pydhcp() (the stop→modify→start pydhcpd cycle) as usual.
|
Una guardia separada, sin relación con la detección de duplicados y nunca fusionada dentro de check_duplicate() — cada función cumple un solo propósito. Se llama junto a check_duplicate(), en los mismos dos puntos (comienzo y final del script). Verifica que ninguna IP de mac-*.txt caiga dentro de un rango reservado para otra cosa. uhm.env solo define dos rangos de IP — UHM_INI_RANGE/UHM_END_RANGE (para uhm-auth.txt) y SERV_INI_RANGE_BLOCK/SERV_END_RANGE_BLOCK (el pool de pydhcp usado por uhm-grace.txt/blockdhcp.txt). Los archivos mac-*.txt son creados y direccionados por el administrador — nada en uhm.env les reserva un rango, así que una IP elegida a mano puede caer fuera de la subred LAN, en la dirección de red/broadcast, sobre el propio SERVER_IP, o dentro de cualquiera de los otros dos rangos. Esto siempre es un error de configuración, sin importar si en ese momento un guest tiene o no esa IP exacta — se reporta con una línea ERROR: puntual, luego exit 1.
Si ninguna de las dos guardias encuentra un problema en la primera pasada, el script continúa directo a is_pydhcp() (el ciclo detener→modificar→arrancar de pydhcpd) normalmente.
|
2026-07-18 20:32:50 ERROR: aa:bb:cc:dd:ee:01: IP inside hotspot range
2026-07-18 20:32:50 ERROR: mac-*.txt IP conflict -- abort
2026-07-18 20:32:50 ERROR: aa:bb:cc:dd:ee:02: IP inside blockdhcp pool
2026-07-18 20:32:50 ERROR: mac-*.txt IP conflict -- abort
| Independent, optional utilities. | Utilidades independientes y opcionales. |
uhmunifi.sh — Authenticates directly against the UniFi controller. It uses UniFi OS (/api/auth/login) by default and, when UNIFI_TYPE=classic, Classic controllers (/api/login). It queries three UniFi datasets:
It logs to /var/log/uhmunifi.log only the summary of each login and query, plus every action performed. Check MAC runs from the terminal on demand only; it produces no continuous logging. The file is truncated at the start of every run, so it holds one session at a time: this is an interactive script, not a daemon, and it needs no rotation of its own. It reads the credentials and other parameters from /etc/uhm/uhm.env. Required variables:
|
uhmunifi.sh — Se autentica directamente contra el controlador UniFi. Por defecto utiliza UniFi OS (/api/auth/login) y, cuando UNIFI_TYPE=classic, utiliza controladores Classic (/api/login). Consulta tres conjuntos de datos de UniFi:
Registra en /var/log/uhmunifi.log únicamente el resumen de cada inicio de sesión y consulta, así como cada acción ejecutada. Check MAC se ejecuta únicamente desde la terminal y bajo demanda; no genera registros continuos. El archivo se vacía al inicio de cada ejecución, así que conserva una sola sesión por vez: es un script interactivo, no un daemon, y no necesita rotación propia. Lee las credenciales y demás parámetros de /etc/uhm/uhm.env. Variables requeridas:
|
Check MAC asks UniFi directly for the current state of a MAC. It shows:
The local files are queried separately through the Local ACL reports of uhmtool.sh, so Check MAC and Local ACL represent different sources of information.
|
Check MAC consulta directamente a UniFi el estado actual de una MAC. Muestra:
Los archivos locales se consultan por separado mediante los reportes Local ACL de uhmtool.sh, por lo que Check MAC y Local ACL representan fuentes de información diferentes.
|
| Action | Description | Descripción |
|---|---|---|
| [1] Delete unused vouchers | Removes vouchers with used=0 (never activated). Safe — no sessions to clean. |
Elimina los vouchers con used=0 (nunca activados). Es una acción segura porque no hay sesiones que limpiar. |
| [2] Forget clients no voucher | Forgets guests who connected to portal but never submitted a voucher. Only affects clients not currently on the SSID, with no voucher record, and not a mac-*.txt device. |
Elimina de UniFi los invitados que llegaron al portal pero nunca canjearon un voucher. Solo afecta a clientes que no estén conectados al SSID, no tengan un registro de voucher y no aparezcan en mac-*.txt. |
| [3] Delete expired vouchers | Deletes vouchers whose end_time has passed, then unauthorizes active sessions and removes the clients' history linked to them. |
Elimina los vouchers cuya end_time ya pasó, desautoriza las sesiones activas y borra el historial de los clientes vinculados. |
| [4] Revoke by voucher code | Revokes one voucher: deletes it if it still exists, unauthorizes active sessions, and removes the client history linked to that code. Addresses an observed UniFi inconsistency: when a voucher is manually deleted from the UniFi UI, stat/guest still retains session records with that voucher_code, allowing affected clients to reconnect without re-entering a code. Cleans everything regardless of whether the voucher still exists in stat/voucher or not. |
Revoca un voucher específico: lo elimina si todavía existe, desautoriza las sesiones activas y borra el historial de clientes vinculado a ese código. Aborda una inconsistencia observada en UniFi: cuando se elimina manualmente un voucher desde la UI de UniFi, stat/guest retiene registros de sesión con ese voucher_code, permitiendo que los clientes afectados se reconecten sin volver a ingresar un código. Limpia todo independientemente de si el voucher aún existe en stat/voucher o no. |
| [5] Forget sessions (!) | Unauthorizes and forgets every active stat/guest session whose authorized_by is not voucher and is not a mac-*.txt device (the UNKNOWN category from ToolView's Guest sessions report). Independent of whether the entry ever reached uhm-auth.txt. |
Desautoriza y olvida toda sesión activa de stat/guest cuyo authorized_by no sea voucher y no sea un dispositivo de mac-*.txt (la categoría UNKNOWN del reporte Guest sessions de ToolView). Independiente de si la entrada llegó a uhm-auth.txt. |
| [6] Purge everything | DESTROYS all vouchers, disconnects all active guests, erases all client history -- excluding mac-*.txt devices, always. Requires typing YES to confirm. Cannot be undone. |
DESTRUYE todos los vouchers, desconecta todos los invitados activos, borra todo el historial de clientes -- excluyendo siempre los dispositivos de mac-*.txt. Requiere escribir YES para confirmar. No se puede deshacer. |
sudo bash /etc/uhm/tools/uhmunifi.sh| Description | Descripción |
|---|---|
| Startup (login + fetch), then a short top-level menu: | Arranque (login + fetch), luego un menú principal corto: |
2026-07-30 15:04:01 uhmunifi start...
============================================================================
AVAILABLE OPTIONS
============================================================================
[1] Check MAC
[2] Actions
[q] Quit
Select option [q]:
[1] Check MAC
Select option [q]: 1
Enter MAC address (XX:XX:XX:XX:XX:XX, empty to cancel): 02:00:00:aa:bb:03
connected
essid=hotspot-example
authorized=true
is_guest=true
ip=192.168.20.103
hostname=guest3-0000000002
voucher_code=0000000002
[2] Actions
Select option [q]: 2
============================================================================
ACTIONS
============================================================================
[1] Delete unused vouchers - never activated
[2] Forget clients no voucher - never used, not connected now
[3] Delete expired vouchers - remove + forget clients
[4] Revoke by voucher code - invalidate one voucher
[5] Forget sessions (!) - unauthorize + forget non-voucher
[6] Purge everything - DELETE all vouchers + history
[b] Back
Select option [b]:
| Description | Descripción |
|---|---|
None of the six actions above ever touch a mac-*.txt MAC -- only the VOUCHER/UNKNOWN categories from ToolView's Guest sessions report are ever eligible. |
Ninguna de las seis acciones de arriba toca jamás una MAC de mac-*.txt -- solo las categorías VOUCHER/UNKNOWN del reporte Guest sessions de ToolView son elegibles. |
uhmalert.sh is an optional, standalone alert watcher. It tails /var/log/uhm.log in real time and sends a push notification via ntfy.sh on two kinds of events: (1) loss of connectivity to the UniFi controller, after UHM_API_FAIL_THRESHOLD consecutive cycles (default 3), followed by a recovery notice once it's back; and (2) any other ERROR or WARNING line in the shared log (from uhmd.sh or the uhmreload.sh/uhmleases.sh/uhmiptables.sh chain) — fires immediately, no threshold.
pydhcpd, uhm's DHCP backend, mirrors a single failure of its own into /var/log/uhm.log: being unable to open its own log file. This lets uhmalert.sh detect and notify it. This is necessary because pydhcpd is an essential component of uhm and has no push-notification system of its own, only log records.
Runs as its own systemd service ( uhmalert.service), independent of uhmd.sh — it never reads or modifies the daemon or its source, only tails the log file it already writes. uhmd.sh stays byte-identical to upstream whether uhmalert is installed or not, and the daemon runs the same with or without it.
|
uhmalert.sh es un supervisor opcional de alertas. Sigue /var/log/uhm.log en tiempo real y envía notificaciones mediante ntfy.sh ante dos tipos de eventos: (1) pérdida de conexión con el controlador UniFi tras UHM_API_FAIL_THRESHOLD ciclos consecutivos (3 por defecto), y envía otro aviso cuando se recupera; y (2) cualquier otra línea ERROR o WARNING en el log compartido (de uhmd.sh o la cadena uhmreload.sh/uhmleases.sh/uhmiptables.sh) -- dispara de inmediato, sin umbral.
pydhcpd, el servidor DHCP de uhm, refleja en /var/log/uhm.log un único fallo propio: no poder abrir su propio log. De esta forma, uhmalert.sh puede detectarlo y notificarlo. Esto es necesario porque pydhcpd es un componente esencial de uhm y no dispone de un sistema propio de alertas para dispositivos móviles, sino únicamente de registro en log.
Se ejecuta como servicio independiente de systemd ( uhmalert.service). No lee ni modifica uhmd.sh: solo sigue el registro que este ya escribe. El daemon funciona igual, esté instalado uhmalert o no.
|
Push notifications via ntfy.sh — See Real Example
Notificaciones push vía ntfy.sh — Ver sección Real Example
Install:
sudo /etc/uhm/tools/uhmalert.sh install==================================
Installing uhmalert (UHM alert)
==================================
Added UHM_NTFY_TOPIC, UHM_API_FAIL_THRESHOLD and
UHM_ALERT_QUIET_PERIOD_SECONDS to /etc/uhm/uhm.env
Deploying script to /etc/uhm/tools/uhmalert.sh...
Writing systemd unit (/etc/systemd/system/uhmalert.service)...
Installed and started. Check with: systemctl status uhmalert
==================================
ntfy topic: uhm-alert-x7k2m9qv
==================================
Install the free 'ntfy' app (Android/iOS) and subscribe to the
topic above to start receiving alerts on this device.
Uninstall:
sudo /etc/uhm/tools/uhmalert.sh uninstall
Detection logic: Successful uhmd cycles are silent (no log output), so there is no positive "cycle OK" line to anchor on. Instead, uhmalert.sh anchors on "Could not load vouchers" -- a line load_all_vouchers() logs exactly once per cycle when the controller is unreachable. Two such lines less than GAP_LIMIT apart count as consecutive failing cycles; a larger gap means cycles succeeded silently in between, and the streak resets (the same GAP_LIMIT is also the read timeout used to detect recovery). GAP_LIMIT = POLL_INTERVAL + 3*API_MAX_TIME + MARGIN (default 20 + 3*30 + 10 = 120s) -- the 3*API_MAX_TIME term covers the worst case of a failed cycle still making up to three 30s-capped API calls (vouchers, guest, sta) before it ends.
Any other line starting with ERROR: or WARNING: fires immediately, no threshold -- the log already classifies severity ("TIMESTAMP LEVEL: message"), shared by uhmd.sh and the uhmreload.sh/uhmleases.sh/uhmiptables.sh chain. Excludes lines already covered by the connectivity streak above (so it still waits for the threshold, not the first failure) and "cycle lock held unexpectedly" (expected, not a bug).
Startup grace: uhmalert.sh itself starts at boot (systemd). If the connectivity threshold is reached while uhmd.service has been active for less than UHM_ALERT_QUIET_PERIOD_SECONDS, the alert is suppressed — UniFi Network/UniFi OS can take a while to come back up after a reboot, and the daemon's very first cycles fail before the controller is even ready to answer. Checked against uhmd's own start time (via systemd), not uhmalert's — so this applies correctly whether the whole machine rebooted or just uhmd restarted on its own. A real outage later on still alerts at the normal threshold, unaffected.
This only covers the run_cycle connectivity streak. The daemon's own initial login (before the first cycle even runs) is handled separately inside uhmd.sh itself, using its own STARTUP_GRACE_SECONDS window — a distinct key from uhmalert.sh's (same default value, 120, but tuning one never silently affects the other) — see the "Daemon Cycle" section below. Startup login retries log at INFO, not ERROR, so they never reach this catch-all in the first place.
Recovery notice guard: a "recovered" notice fires only if uhmd.service is still active when the GAP_LIMIT silence window elapses. Silence has two indistinguishable causes — cycles actually recovered, or the daemon stopped writing to the log entirely (manual stop, crash, start-limit-hit) — and without this check the second case would still send a false "recovered" notice while the controller could still be down and the daemon not even running.
|
Lógica de detección: Los ciclos exitosos de uhmd no escriben en el registro. Por eso, uhmalert.sh detecta los fallos a partir de "Could not load vouchers" -- una linea que load_all_vouchers() registra exactamente una vez por ciclo cuando el controlador es inalcanzable. Dos de esas lineas separadas por menos de GAP_LIMIT cuentan como ciclos fallidos consecutivos; un salto mayor implica que hubo ciclos exitosos silenciosos en el medio, y la racha se reinicia (el mismo GAP_LIMIT es también el timeout de lectura usado para detectar la recuperación). GAP_LIMIT = POLL_INTERVAL + 3*API_MAX_TIME + MARGIN (default 20 + 3*30 + 10 = 120s) -- el término 3*API_MAX_TIME cubre el peor caso de un ciclo fallido que aún así hace hasta tres llamadas API con límite de 30s (vouchers, guest, sta) antes de terminar.
Cualquier otra línea que empiece con ERROR: o WARNING: genera una alerta inmediata, sin umbral -- el log ya clasifica la severidad ("TIMESTAMP NIVEL: mensaje"), compartido entre uhmd.sh y la cadena uhmreload.sh/uhmleases.sh/uhmiptables.sh. Excluye las lineas ya cubiertas por la racha de conectividad de arriba (para que siga esperando el umbral, no el primer fallo) y "cycle lock held unexpectedly" (esperado, no es un bug).
Gracia de arranque: uhmalert.sh arranca junto con el sistema (systemd). Si el umbral de conectividad se cumple mientras uhmd.service lleva menos de UHM_ALERT_QUIET_PERIOD_SECONDS activo, la alerta se suprime -- UniFi Network/UniFi OS puede tardar en volver a estar disponible tras un reinicio, y los primeros ciclos del daemon fallan antes de que el controlador siquiera esté listo para responder. Se verifica contra el propio inicio de uhmd (vía systemd), no el de uhmalert -- asi aplica correctamente ya sea que se haya reiniciado el equipo completo o solo uhmd por su cuenta. Un fallo real más adelante sigue alertando con el umbral normal, sin verse afectado.
Esto solo cubre la racha de conectividad de run_cycle. El login inicial del daemon (antes de que corra el primer ciclo) se maneja aparte, dentro del propio uhmd.sh, usando su propia ventana STARTUP_GRACE_SECONDS -- una clave distinta a la de uhmalert.sh (mismo valor por defecto, 120, pero ajustar una nunca afecta a la otra en silencio) -- ver la sección "Daemon Cycle" más abajo. Los reintentos de login de arranque quedan en nivel INFO, no ERROR, así que nunca llegan a este catch-all.
Verificación antes del aviso de recuperación: un aviso de "recovered" solo se envía si uhmd.service sigue activo cuando se cumple la ventana de silencio GAP_LIMIT. El silencio tiene dos causas indistinguibles -- los ciclos realmente se recuperaron, o el daemon dejó de escribir en el log por completo (detención manual, crash, start-limit-hit) -- y sin este chequeo el segundo caso igual mandaría un falso "recovered" mientras el controlador podría seguir caído y el daemon ni siquiera estar corriendo.
|
A brief controller outage (restart/update) triggers exactly the sequence shown in the screenshot above. The daemon degrades gracefully on every failed cycle — sessions step ... -- skip/revoke step ... -- skip — instead of acting on partial data, alerts once the 3-cycle threshold is hit, and re-authenticates automatically once the controller is reachable again:
|
Una caída breve del controlador (reinicio/actualización) dispara exactamente la secuencia del pantallazo de arriba. El daemon se degrada de forma segura en cada ciclo fallido — sessions step ... -- skip/revoke step ... -- skip — en vez de actuar con datos parciales, alerta al llegar al umbral de 3 ciclos, y se re-autentica solo apenas el controlador vuelve a responder:
|
2026-07-12 00:40:26 INFO: API GET stat/voucher -> HTTP 502 -- skip
2026-07-12 00:40:26 INFO: Could not load vouchers (rc=empty) -- skip
2026-07-12 00:40:28 INFO: API GET stat/guest -> HTTP 000 -- skip
2026-07-12 00:40:28 INFO: sessions step, stat/guest unavailable -- skip
2026-07-12 00:40:29 INFO: API GET stat/sta -> HTTP 000 -- skip
2026-07-12 00:40:29 INFO: revoke step, stat/sta unavailable -- skip
[... cycles keep failing every ~POLL_INTERVAL, same pattern ...]
2026-07-12 00:41:11 INFO: Could not load vouchers (rc=empty) -- skip
2026-07-12 00:41:11 INFO: 3 consecutive cycle failures
2026-07-12 00:41:11 INFO: latest at 2026-07-12 00:41:11
[... failures continue while the controller is still down ...]
2026-07-12 00:43:13 INFO: recovery notice (no new failures)
2026-09-25 19:41:46 INFO UniFi login OK
2026-09-25 19:41:46 INFO session expired, re-authenticating
2026-09-25 19:33:20 STATUS vouchers=6|auth=27|grace=8|newauth=0|revoked=0
2026-09-25 19:33:20 STATUS uhmreload done at: 2026-09-25 19:33:20
2026-09-25 19:33:20 STATUS uhmiptables done at: 2026-09-25 19:33:20
2026-09-25 19:33:11 STATUS uhmiptables start...
2026-09-25 19:33:10 STATUS uhmleases done at: 2026-09-25 19:33:10
2026-09-25 19:33:10 STATUS blockdhcp=473|limited=97|unlimited=34|hotspot=27|grace=8
The first HTTP 502 (proxy up, backend not yet) followed immediately by HTTP 000 on every subsequent request (connection itself unreachable) is the fingerprint of a UniFi OS controller restart, not a network/firewall problem on the UHM side — worth checking the controller's own system log for that window if it happens outside a planned update.
A server reboot shows a different, unrelated-looking pattern instead — quiet INFO-level login retries while UniFi OS is still booting, followed by a login success, followed by a few data-endpoint failures before the backend settles — with no alert firing, since uhmalert.sh is also inside its own startup grace window at that point. See uhmd above for that log sequence in full.
|
El primer HTTP 502 (proxy activo, backend aún no) seguido de inmediato por HTTP 000 en cada petición posterior (la conexión misma es inalcanzable) es la firma de un reinicio del controlador UniFi OS, no un problema de red/firewall del lado de UHM — vale la pena revisar el log propio del sistema del controlador en esa ventana si ocurre fuera de una actualización planificada.
Un reinicio del servidor muestra un patrón distinto y aparentemente no relacionado — reintentos de login silenciosos en nivel INFO mientras UniFi OS todavía está arrancando, seguidos de un login exitoso, seguidos de algunos fallos en los endpoints de datos antes de que el backend se asiente — sin que se dispare ninguna alerta, ya que uhmalert.sh también está dentro de su propia ventana de gracia de arranque en ese momento. Ver uhmd arriba para esa secuencia de log completa.
|
Configuration variables (in uhm.env, written automatically by install):
| Variable | Default | Description | Descripción |
|---|---|---|---|
UHM_NTFY_TOPIC |
(auto-generated) | ntfy.sh topic name, e.g. uhm-alert-x7k2m9qv. Treat as a shared secret — anyone who knows it can publish to it. Never overwritten by a re-install. |
Nombre del topic de ntfy.sh, ej. uhm-alert-x7k2m9qv. Trátelo como un secreto compartido — cualquiera que lo conozca puede publicar en él. Nunca se sobrescribe en una reinstalación. |
UHM_API_FAIL_THRESHOLD |
3 | Consecutive failing cycles required before sending an alert | Ciclos fallidos consecutivos requeridos antes de enviar una alerta |
UHM_ALERT_QUIET_PERIOD_SECONDS |
120 | Suppresses the connectivity alert while uhmd.service has been active for less than this long — UniFi Network/UniFi OS can take a while to come back up after a reboot, and this host often boots alongside it. Written to uhm.env by uhmalert.sh install. Separate from uhmd.sh's own STARTUP_GRACE_SECONDS (same default, different key, tuning one never affects the other). This is an estimate, not a measured value: tune it to how long your UniFi Network/UniFi OS instance actually takes to come back up after a restart. Only the startup window is affected — a real outage later in the day still alerts at the normal threshold, undiminished. |
Suprime la alerta de conectividad mientras uhmd.service ha estado activo por menos de este tiempo — UniFi Network/UniFi OS puede tardar en volver tras un reinicio, y este host suele arrancar junto con él. Escrito en uhm.env por uhmalert.sh install. Separada de la propia STARTUP_GRACE_SECONDS de uhmd.sh (mismo default, clave distinta, ajustar una nunca afecta a la otra). Esto es una estimación, no un valor medido: ajústelo a lo que realmente tarda su instancia de UniFi Network/UniFi OS en volver tras un reinicio. Solo afecta la ventana de arranque — un corte real más tarde en el día sigue alertando en el umbral normal, sin disminución. |
POLL_INTERVALis read from the sameuhm.envused byuhmd.sh(falls back to 20 if unset) — no separate configuration needed.
POLL_INTERVALse lee del mismouhm.envque usauhmd.sh(default 20 si no esta definido) -- no requiere configuracion aparte.
uhmwatch.sh is a mandatory, standalone services watchdog — installed automatically by uhmsetup.sh, not offered as a yes/no prompt like uhmalert or the web interface. Every unit it watches already has its own systemd Restart= policy, but that alone gives up permanently once its StartLimitBurst is exhausted, with no further attempt and no alert of its own (see below). uhmwatch is the last line of defense against that — it runs every minute, independent of whatever state systemd itself gave up in, so UHM's essential services don't stay down indefinitely just because systemd stopped trying. Checks every service UHM depends on, restarting whichever is down: uhmd.service (always), uhmalert.service (only if installed), pydhcpd.service (always -- external dependency UHM cannot function without, watched here since pydhcp's own Restart=on-failure gives up silently after its burst with no alerting of its own), and the UniFi backend (uosserver.service for UNIFI_TYPE=unifi-os, or unifi.service for classic). Each check is fully independent — one check's failure never skips or blocks the others in the same run. Each recovery attempt runs systemctl reset-failed right before start/restart — every unit already carries its own Restart= policy with a StartLimitBurst, and once that burst is exhausted systemd stops trying on its own and stays quiet about it, which would otherwise make this watchdog's own restart attempt fail silently right when it's needed most. To avoid then hammering a persistently broken service every single minute, each restart attempt (successful or not) is timestamped per-service under /run/uhmwatch/ (cleared on reboot), and a new attempt is skipped — logged only, not acted on — until RECOVERY_COOLDOWN_SECONDS (default 600s / 10 min) has passed since the last one.
Standalone — never reads or modifies uhmd.sh, only manages services via systemctl. Writes to the same shared /var/log/uhm.log as the rest of UHM (no separate log file or logrotate of its own). Silent on a healthy run — nothing is logged unless a check finds a problem or takes a fix action.
The pydhcpd.service check specifically skips its "OFFLINE" verdict (no WARNING, no restart) if uhmleases.sh currently holds the same cycle lock uhmd.sh uses (/var/lock/uhmd-cycle.lock) — a normal reload stops/reconfigures/starts pydhcpd itself for a few seconds, and a cron tick landing in that window would otherwise "fix" a service that isn't actually broken, restarting it out from under uhmleases.sh's own pending restart and aborting that reload.
The UniFi backend gets a functional check, not just is-active. The unit must be active first; if it is not, it is started. Then a real login runs against the API, the same mechanism uhmd.sh uses — credentials passed to jq through the environment and the payload to curl through stdin, never in argv. HTTP 200 means healthy. HTTP 000 or any 5xx means unresponsive and the service is restarted, except within STARTUP_GRACE_SECONDS of uhmd.service's own start: there it is logged as INFO and nothing is restarted, because the controller is expected to still be booting after a reboot. HTTP 429 is logged as rate limiting, not a credentials problem. Any other 4xx means the credentials were rejected while the service itself is up and answering — logged as a WARNING with no restart, since a restart cannot fix a wrong password in uhm.env. If UNIFI_USERNAME or UNIFI_PASSWORD is not set, the login check is skipped and a listening-port check takes its place: port 11443 for unifi-os, ports 8443 or 8080 for classic. pydhcpd gets no functional check of this kind, because it exposes no HTTP API to probe.
|
uhmwatch.sh es un vigilante de servicios obligatorio e independiente — se instala automáticamente con uhmsetup.sh, no se ofrece como pregunta sí/no como uhmalert o la interfaz web. Cada unidad vigilada tiene su propia política Restart= de systemd, pero systemd deja de reintentar cuando alcanza StartLimitBurst. uhmwatch es una capa adicional de recuperación: se ejecuta cada minuto e intenta iniciar de nuevo los servicios que encuentra detenidos. Revisa de forma independiente los servicios necesarios para UHM e intenta recuperar los que encuentra detenidos: uhmd.service (siempre), uhmalert.service (solo si está instalado), pydhcpd.service (siempre -- dependencia externa sin la cual UHM no puede funcionar, vigilada acá porque el propio Restart=on-failure de pydhcp se rinde en silencio tras agotar su cupo, sin ningún aviso propio), y el backend de UniFi (uosserver.service para UNIFI_TYPE=unifi-os, o unifi.service para classic). Cada intento ejecuta systemctl reset-failed antes de iniciar o reiniciar el servicio. Para evitar intentos repetidos contra una falla persistente, registra cada intento en /run/uhmwatch/, tanto si tiene éxito como si falla. Espera RECOVERY_COOLDOWN_SECONDS (600 segundos por defecto) antes de volver a intentarlo; las marcas se eliminan al reiniciar el servidor.
Independiente — nunca lee ni modifica uhmd.sh, solo gestiona servicios vía systemctl. Escribe al mismo /var/log/uhm.log compartido con el resto de UHM (sin log ni logrotate propio). Silencioso en una corrida sana — no registra nada salvo que un chequeo encuentre un problema o tome una acción de reparación.
El chequeo de pydhcpd.service específicamente se salta el veredicto "OFFLINE" (sin WARNING, sin restart) si uhmleases.sh tiene tomado en ese momento el mismo lock de ciclo que usa uhmd.sh (/var/lock/uhmd-cycle.lock) — un reload normal detiene/reconfigura/arranca pydhcpd él mismo durante unos segundos, y una corrida de cron que caiga en esa ventana de otro modo "arreglaría" un servicio que no está realmente roto, reiniciándolo por debajo del restart que uhmleases.sh ya tenía pendiente y abortando ese reload.
El backend de UniFi recibe un chequeo funcional, no solo is-active. Primero la unidad debe estar activa; si no lo está, se inicia. Después se ejecuta un login real contra la API, el mismo mecanismo que usa uhmd.sh — las credenciales llegan a jq por el entorno y el payload a curl por stdin, nunca en argv. HTTP 200 significa sano. HTTP 000 o cualquier 5xx significa que no responde y el servicio se reinicia, salvo dentro de STARTUP_GRACE_SECONDS desde el arranque de uhmd.service: ahí se registra como INFO y no se reinicia nada, porque se espera que el controlador todavía esté arrancando tras un reinicio. HTTP 429 se registra como límite de tasa, no como problema de credenciales. Cualquier otro 4xx significa que las credenciales fueron rechazadas mientras el servicio está arriba y respondiendo — se registra como WARNING sin reiniciar, porque un reinicio no corrige una contraseña equivocada en uhm.env. Si UNIFI_USERNAME o UNIFI_PASSWORD no están definidos, el chequeo de login se omite y en su lugar se verifica el puerto a la escucha: el 11443 para unifi-os, los puertos 8443 u 8080 para classic. pydhcpd no recibe un chequeo funcional de este tipo, porque no expone ninguna API HTTP que sondear.
|
Install:
sudo /etc/uhm/core/uhmwatch.sh install==================================
Installing uhmwatch (UHM services watchdog)
==================================
Deploying script to /etc/uhm/core/uhmwatch.sh...
Cron entry registered: * * * * * /etc/uhm/core/uhmwatch.sh
Installed. First run happens on the next minute mark.
Check the log with: tail -f /var/log/uhm.log
uhmwatch.sh is silent on a healthy run -- nothing is logged unless a check finds a problem. Example of what a detected-and-fixed failure looks like in /var/log/uhm.log / uhmwatch.sh es silencioso en una corrida sana -- no registra nada a menos que un chequeo encuentre un problema. Ejemplo de cómo se ve una falla detectada y corregida en /var/log/uhm.log:
2026-07-29 21:18:18 WARNING: uhmd OFFLINE -- alert
2026-07-29 21:18:18 INFO: uhmd restarted
If uhmalert.sh is also installed, the WARNING: line reaches your phone as a push notification — the INFO: recovery line does not, uhmalert.sh only forwards ERROR:/WARNING: lines. uhmwatch.sh and uhmalert.sh are independent, but this is what having both installed together looks like in practice / Si uhmalert.sh también está instalado, la línea WARNING: te llega al teléfono como notificación push — la línea INFO: de recuperación no, uhmalert.sh solo reenvía líneas ERROR:/WARNING:. uhmwatch.sh y uhmalert.sh son independientes, pero así se ve en la práctica tenerlos instalados juntos:
The notification app may not display messages in chronological order (it can group same-minute notifications arbitrarily). Since it's only a notification, the recommendation is to check
/var/log/uhm.logfor the actual event order.Es posible que la app de notificaciones no muestre los mensajes en orden cronológico (puede agrupar notificaciones del mismo minuto de forma arbitraria). Al ser solo una notificación, se recomienda revisar
/var/log/uhm.logpara ver el orden real de los eventos.
Uninstall:
sudo /etc/uhm/core/uhmwatch.sh uninstall
UniFi backend check: a plain systemctl is-active only proves the process is up, not that the application itself is healthy — the container's (or subprocess's) embedded MongoDB can fail to come up while the process keeps running, leaving every real API call broken. So once the service is confirmed active, uhmwatch.sh performs the same real login uhmd.sh itself relies on (UNIFI_USERNAME/UNIFI_PASSWORD from uhm.env, credentials via jq env and payload via curl stdin — never in argv). HTTP 200 = healthy. HTTP 000 (unreachable) or 5xx (server error) = unresponsive, restarts the service. HTTP 429 means the controller itself is rate-limiting login attempts — logged as a distinct warning, no restart (see Controller lockout below). Any other 4xx means credentials rejected but service online — logged as a warning, no restart. Possible causes: wrong UNIFI_USERNAME/UNIFI_PASSWORD in uhm.env, or an account that is locked, expired, or has 2FA enabled (see 2FA and Remote Access above). If UNIFI_USERNAME/UNIFI_PASSWORD aren't set, falls back to a process/port-only check instead of skipping it.
|
Chequeo del backend UniFi: un simple systemctl is-active solo prueba que el proceso está arriba, no que la aplicación esté sana — el MongoDB embebido del contenedor (o subproceso) puede fallar al iniciar mientras el proceso sigue corriendo, dejando rota cualquier llamada real a la API. Por eso, una vez confirmado que el servicio está activo, uhmwatch.sh hace el mismo login real que usa uhmd.sh (UNIFI_USERNAME/UNIFI_PASSWORD de uhm.env, credenciales vía env de jq y payload vía stdin de curl — nunca en argv). HTTP 200 = sano. HTTP 000 (inalcanzable) o 5xx (error de servidor) = no responde, reinicia el servicio. HTTP 429 significa que el propio controlador está limitando la tasa de intentos de login — se registra como advertencia distinta, sin reiniciar (ver Bloqueo del controlador abajo). Cualquier otro 4xx significa credenciales rechazadas pero servicio online — se registra como advertencia, sin reiniciar. Posibles causas: UNIFI_USERNAME/UNIFI_PASSWORD incorrecto en uhm.env, o cuenta bloqueada, caducada, o con 2FA activo (ver 2FA and Remote Access arriba). Si UNIFI_USERNAME/UNIFI_PASSWORD no están configuradas, cae de vuelta a un chequeo de solo proceso/puerto en vez de omitirlo.
|
Wrong password / Contraseña incorrecta:
2026-07-15 17:21:03 WARNING: credentials rejected (HTTP 403)
2026-07-15 17:21:03 WARNING: check uhm.env, UOS is responding -- alert
Controller lockout (HTTP 429) / Bloqueo del controlador (HTTP 429):
# from uhmd.sh, repeating every 10s during its own startup retry loop:
2026-07-31 23:57:13 INFO: UniFi login failed (HTTP 429) in grace -- skip
2026-07-31 23:57:23 INFO: UniFi login failed (HTTP 429) in grace -- skip
...
2026-07-31 23:59:04 INFO: UniFi login failed (HTTP 429) in grace -- skip
2026-07-31 23:59:04 ERROR: no UniFi login in 120s -- abort
# from uhmwatch.sh, on its next check:
2026-07-31 23:59:15 WARNING: rate limited (HTTP 429), not a credentials issue
2026-07-31 23:59:15 WARNING: stop uhmd and uhmwatch cron -- alert
HTTP 429 means the controller is throttling login attempts -- it is not a wrong password, and restarting a service will not fix it, it can make it worse. It typically happens after several rapid failed login attempts in a short window (UniFi's own anti-brute-force protection), and it is self-sustaining: uhmd.service ships with Restart=always/RestartSec=10, and uhmd.sh itself retries login every 10s for up to STARTUP_GRACE_SECONDS (default 120s) before exiting -- if the controller is already rate-limiting, this loop keeps re-triggering the lockout indefinitely, and uhmwatch.sh's own 1-minute restart of uhmd.service (if it finds it down) feeds the same loop.
Recovery procedure:
|
HTTP 429 significa que el controlador está limitando la tasa de intentos de login -- no es una contraseña incorrecta, y reiniciar un servicio no lo arregla, puede empeorarlo. Suele ocurrir después de varios intentos fallidos rápidos en poco tiempo (protección anti-fuerza-bruta propia de UniFi), y es autosostenido: uhmd.service viene con Restart=always/RestartSec=10, y uhmd.sh reintenta el login cada 10s durante hasta STARTUP_GRACE_SECONDS (default 120s) antes de salir -- si el controlador ya está limitando la tasa, este loop sigue disparando el bloqueo indefinidamente, y el propio reinicio de uhmd.service que hace uhmwatch.sh cada minuto (si lo encuentra caído) alimenta el mismo loop.
Procedimiento de recuperación:
|
Normal operation / Operación normal:
(nothing — a healthy run writes no log lines / nada — una corrida sana no escribe líneas de log)
uhm.log — All output from every component (uhmd, uhmreload.sh, uhmleases.sh, uhmwatch.sh, uhmalert.sh, uhmiptables.sh) is unified in /var/log/uhm.log and rotated via /etc/logrotate.d/uhm (daily, 7 rotations, compressed). The log stays silent during cycles with no changes. It records state changes, warnings, and errors with levels INFO:, WARNING:, or ERROR:. LogView groups unlabelled counters and start/end markers under STATUS. Before writing an active cycle, uhmd adds a separator line.
|
uhm.log — Todos los componentes escriben en /var/log/uhm.log. El archivo se rota a diario y conserva siete copias comprimidas según /etc/logrotate.d/uhm. Los ciclos sin cambios no generan líneas; cuando hay cambios, advertencias o errores, el registro indica el nivel (INFO:, WARNING: o ERROR:). LogView agrupa los contadores y las marcas de inicio y fin, que no tienen nivel, bajo la etiqueta STATUS. uhmd añade una línea separadora antes de registrar un ciclo con actividad.
|
| Level | Description | Descripción |
|---|---|---|
ERROR: |
Exclusively for a message that aborts the current flow -- the script or the calling function stops right there, nothing after it runs. Always paired with the -- abort suffix. |
Exclusivo para un mensaje que aborta el flujo actual -- el script o la función que lo invoca 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 script 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 script usó un valor por defecto en su lugar -- ese valor debe corregirse). |
INFO: |
Routine state changes and notifications -- everything else, including anything skipped, defaulted, or self-healed without needing administrator attention. Flag is optional, used only when necessary: -- skip, -- retry, -- fixed, or none. |
Cambios de estado rutinarios y notificaciones -- todo lo demás, incluyendo lo omitido, resuelto con un valor por defecto, o auto-reparado sin necesitar atención del administrador. El flag es opcional, solo cuando es necesario: -- skip, -- retry, -- fixed, o ninguno. |
STATUS (no prefix) |
Level-less lines: each script's own "<name> start..."/"<name> done" boundary markers, and the compact field=value|field=value counters -- grouped under this generic label only by the LogView tab of the web interface, not written as STATUS: in the log itself. |
Líneas sin nivel: las marcas de inicio/cierre "<nombre> start..."/"<nombre> done" de cada script, y los contadores compactos campo=valor|campo=valor -- agrupadas bajo esta etiqueta genérica solo por la pestaña LogView de la interfaz web, no se escriben como STATUS: en el log real. |
uhmalert.shsends push notifications only forERROR:/WARNING:lines. For pydhcp's own log format and levels, see pydhcp -- Log levels.
uhmalert.shenvía notificaciones push solo para líneasERROR:/WARNING:. Para el formato y niveles de log propios de pydhcp, ver pydhcp -- Log levels.
| Level | What happens | Qué ocurre | Example |
|---|---|---|---|
| (no level) | Start/end markers and per-cycle totals | Marcas de inicio y fin, y totales por ciclo | uhmleases start... · blockdhcp=67|limited=105|... |
INFO: |
One line per state change | Una línea por cambio de estado | new client X -> grace · Authorized X · kicked X |
INFO: ... -- skip |
The step is skipped and retried next cycle | El paso se salta y se reintenta en el siguiente ciclo | API GET stat/sta -> HTTP 000 -- skip |
INFO: |
Logged once, when all three endpoints answer together | Se registra una vez, cuando los tres endpoints responden juntos | UniFi backend ready (voucher/guest/sta OK) |
WARNING: ... -- fallback |
The documented default is used | Se usa el valor por defecto documentado | no CLEANUP_INTERVAL in pydhcp.env -- fallback |
INFO: ... -- fixed |
Self-healed, nothing for the admin to do | Auto-reparado, nada que el administrador deba hacer | uhm.env perms fixed -- fixed |
WARNING: ... -- alert |
The MACs stay queued and are harmlessly reprocessed next cycle -- never a permissions issue (runs as root); check free space, a read-only mount, or the immutable attribute (lsattr, cleared with chattr -i) |
Los MACs quedan en cola y se reprocesan sin efecto en el siguiente ciclo -- nunca es un problema de permisos (corre como root); revise espacio libre, montaje de solo lectura, o el atributo de inmodificable (lsattr, se quita con chattr -i) |
cannot empty uhm-queue.txt -- alert |
WARNING: ... -- alert |
The previous config is restored; the next cycle retries | Se restaura la configuración anterior; el siguiente ciclo reintenta | uhmreload failed (code 1), back off -- alert |
WARNING: ... -- alert |
uhmwatch.sh found the service down |
uhmwatch.sh encontró el servicio caído |
pydhcpd OFFLINE -- alert · uhmd restart FAILED -- alert |
ERROR: ... -- abort |
The script stops before touching anything | El script se detiene antes de tocar nada | missing dependency 'jq' -- abort · uhm.env not found, run uhmsetup.sh -- abort |
ERROR: ... -- abort |
Every offending entry is listed before aborting | Se listan todas las entradas implicadas antes de abortar | mac-*.txt IP conflict -- abort |
--------------------------------------------------------------------------------
2026-07-01 06:47:35 INFO: new client 02:00:00:aa:bb:10 -> grace
2026-07-01 06:47:35 INFO: ip=192.168.0.231 host=no_name_fde07d34be
2026-07-01 06:47:35 INFO: added 1 new client(s) to uhm-grace
2026-07-01 06:47:35 INFO: uhm-grace.txt changed
2026-07-01 06:47:35 INFO: invoking /etc/uhm/core/uhmreload.sh
2026-07-01 06:47:35 uhmreload start...
2026-07-01 06:47:35 uhmleases start...
2026-07-01 06:47:36 INFO: 02:00:00:aa:bb:11 expired (age=43346s)
2026-07-01 06:47:36 INFO: add 02:00:00:aa:bb:11 to blockdhcp
2026-07-01 06:47:36 INFO: queued removal for 02:00:00:aa:bb:11
2026-07-01 06:47:40 blockdhcp=67|limited=105|unlimited=35|hotspot=17|grace=8
2026-07-01 06:47:40 uhmleases done at: 2026-07-01 06:47:40
2026-07-01 06:47:40 uhmiptables start...
2026-07-01 06:47:42 uhmiptables done at: 2026-07-01 06:47:42
2026-07-01 06:47:42 uhmreload done at: 2026-07-01 06:47:42
2026-07-01 06:47:42 vouchers=3|auth=17|grace=8|newauth=0|revoked=0
When no client connects, no voucher is redeemed, and no grace entry expires, the log between two cycles is simply empty -- nothing is written.
Cuando no hay cliente conectado, ningún voucher canjeado, ni ninguna entrada de gracia expirada, el log entre dos ciclos queda simplemente vacío: no se escribe nada.
| Reload failure and backoff | Fallo de reload y backoff |
A safety backoff against an error in some line of the scripts uhmreload.sh invokes (especially uhmiptables.sh, which is outside the scope of this project). If UHM_RELOAD (uhmreload.sh) fails or times out, uhmd logs the failure and switches to "backing off to safety-net cadence": it will not retry on the next cycle (every POLL_INTERVAL) — it waits the full RELOAD_SAFETY_INTERVAL_SECONDS (default 3600s = 1h) before invoking the reload chain again, so a persistent failure does not spam the log or re-alert every cycle. The same backoff also fires if UHM_RELOAD is missing. Any line prefixed WARNING: or ERROR: in uhm.log is picked up by uhmalert.sh (see uhmalert), which sends it as a push notification and writes its own INFO: line to the log, stripped of the original label and action, confirming it already notified you.
|
Un backoff de seguridad ante un error en alguna línea de los scripts que invoca uhmreload.sh (especialmente uhmiptables.sh, que está fuera del alcance de este proyecto). Si UHM_RELOAD (uhmreload.sh) falla o hace timeout, uhmd registra el fallo y pasa a "backing off to safety-net cadence": no reintenta en el siguiente ciclo (cada POLL_INTERVAL) — espera el RELOAD_SAFETY_INTERVAL_SECONDS completo (default 3600s = 1h) antes de invocar de nuevo la cadena de reload, para que un fallo persistente no sature el log ni vuelva a alertar en cada ciclo. El mismo backoff también ocurre si UHM_RELOAD falta. Cualquier línea con prefijo WARNING: o ERROR: en uhm.log es detectada por uhmalert.sh (ver uhmalert), que la envía como notificación push y escribe su propia línea INFO: en el log, sin la etiqueta ni la acción original, confirmando que ya te avisó.
|
2026-07-27 20:45:28 WARNING: uhmreload failed (code 1), back off -- alert
2026-07-27 20:45:29 INFO: uhmreload failed (code 1), back off
| Field | Type | Description | Descripción |
|---|---|---|---|
vouchers |
total | Vouchers currently in UniFi (stat/voucher) |
Vouchers presentes en UniFi |
auth |
total | MACs in uhm-auth.txt at end of cycle |
MACs en uhm-auth.txt al final del ciclo |
grace |
total | MACs in uhm-grace.txt at end of cycle |
MACs en uhm-grace.txt al final del ciclo |
newauth |
delta | MACs processed by the sessions step this cycle: new promotions to uhm-auth.txt and voucher renewals of MACs already in it (only new promotions get kicked — see step 10) |
MACs procesadas por el paso de sesiones en este ciclo: promociones nuevas a uhm-auth.txt y renovaciones de voucher de MACs ya presentes en él (solo las promociones nuevas reciben kick — ver paso 10) |
revoked |
delta | MACs removed from uhm-auth.txt this cycle (authorized=false in UniFi) |
MACs eliminadas de uhm-auth.txt en este ciclo |
uhmleases output — Written to /var/log/uhm.log (unified log). Only real state changes on uhm-grace.txt are logged: a MAC added on first contact, one expired to blockdhcp.txt after BLOCKDHCP_GRACE_SECONDS, or one removed by check_duplicate() when found in another ACL list. Entries that are simply preserved during their grace period produce no output — nothing to log means nothing changed.
|
Salida de uhmleases — Se escribe en /var/log/uhm.log (log unificado). Solo se registran cambios reales de estado sobre uhm-grace.txt: una MAC agregada al primer contacto, una expirada a blockdhcp.txt tras BLOCKDHCP_GRACE_SECONDS, o una removida por check_duplicate() al encontrarse en otra lista ACL. Las entradas que simplemente se preservan durante su período de gracia no producen ninguna salida — nada que registrar significa que nada cambió.
|
2026-07-01 06:47:36 INFO: 02:00:00:aa:bb:11 expired (age=43346s)
2026-07-01 06:47:36 INFO: add 02:00:00:aa:bb:11 to blockdhcp
2026-07-01 06:47:36 INFO: queued removal for 02:00:00:aa:bb:11
| UniFi controller access log | Log de acceso del controlador UniFi |
Separate from /var/log/uhm.log. UniFi OS Server runs inside a Podman container (uosserver), so its own portal access log lives at /data/unifi/logs/access.log inside that container, not on the host. Useful to confirm whether a client's captive-portal probe actually reached the AP's native redirect (look for ap=, id=, ssid= in the URL — their absence means the hit didn't come from the AP redirect). It's a binary-ish log file, so use grep -a.
|
Distinto de /var/log/uhm.log. UniFi OS Server corre dentro de un contenedor Podman (uosserver), así que su propio log de acceso al portal vive en /data/unifi/logs/access.log dentro de ese contenedor, no en el host. Útil para confirmar si el sondeo de portal cautivo de un cliente realmente llegó al redirect nativo del AP (busque ap=, id=, ssid= en la URL — su ausencia significa que el hit no vino del redirect del AP). Es un archivo de log cuasi-binario, use grep -a.
|
# Tail live, filtering only captive-portal hits (/guest/)
sudo -u uosserver podman exec uosserver tail -f /data/unifi/logs/access.log \
| grep --line-buffered -a "/guest/"
# Example line this produces (302 = AP redirect worked, params present):
# [2026-07-04T15:02:33,854-05:00] [ 192.168.0.231 -> portal-82 ] GET 200 3ms \
# /guest/s/default/?ap=02:00:00:aa:bb:12&id=02:00:00:aa:bb:13&t=1783195353&url=http://netcts.cdn-apple.com%2F&ssid=EXAMPLE_SSID
# Search the full history for a specific client MAC (not IP — IPs rotate every DHCP renewal)
sudo -u uosserver podman exec uosserver grep -a "id=02:00:00:aa:bb:13" /data/unifi/logs/access.log
# Confirm the portal itself is reachable and serving (run from the gateway host)
sudo -u uosserver podman exec uosserver curl -v http://192.168.0.10:8880/guest/s/default/| Note | Description | Descripción |
|---|---|---|
| Synchronization | UHM depends on correct synchronization between UniFi Network, the DHCP server, and the user-maintained firewall script. It is not guaranteed to work on every Linux system. |
UHM depende de la correcta sincronización entre UniFi Network, el servidor DHCP y el script firewall que mantiene el usuario. No se garantiza su funcionamiento en todos los sistemas Linux. |
| Lease queue | The script queues lease removals for MACs it manages (via uhm-queue.txt). Actual removal is performed by uhmleases.sh during its safe DHCP stop→modify→start cycle. Leases for hotspot MACs are short-lived by design. uhm-queue.txt's path comes from the UHM_QUEUE config variable; it is an internal working file consumed by both scripts, not an ACL — do not edit its contents manually. |
El script encola remociones de leases para los MACs que gestiona (vía uhm-queue.txt). La remoción real la ejecuta uhmleases.sh durante su ciclo seguro de detener→modificar→arrancar DHCP. Los leases para MACs del hotspot son de corta vida por diseño. La ruta de uhm-queue.txt la fija la variable de configuración UHM_QUEUE; es un archivo de trabajo interno que consumen ambos scripts, no una ACL — no debe editarse su contenido manualmente. |
| Firewall scope | Both uhm-grace.txt and uhm-auth.txt clients must be reachable via your DHCP server. Only uhm-auth.txt clients should be granted full Internet by your firewall; grace-period clients (macgrace ipset) should only reach the captive portal ports. |
Los clientes de uhm-grace.txt y uhm-auth.txt deben ser alcanzables por su servidor DHCP. Solo uhm-auth.txt debe tener Internet completo vía firewall; los clientes en período de gracia (ipset macgrace) solo deben llegar a los puertos del portal cautivo. |
| Script header | Read the script header before deploying — it documents the full flow and any newly added behavior. | Lea el header del script antes de desplegarlo — documenta el flujo completo y cualquier comportamiento recién añadido. |
| Testing | Always test in a non-production environment first. | Pruebe siempre en un entorno no productivo primero. |
| WPAD/PAC | uhmleases.sh generates /etc/pydhcp/core/pydhcpd.conf dynamically on every run. Set WPAD_ENABLED=true in uhm.env to enable WPAD/PAC via DHCP option 252, and WPAD_PORT to the port your Apache VirtualHost listens on (default 18100). Prerequisites, to be in place before setting true: Apache2 installed, a VirtualHost listening on WPAD_PORT with that port declared in Apache's ports.conf as Listen SERVER_IP:PORT, and a valid wpad.pac in its document root. Guard: uhmleases.sh never trusts WPAD_ENABLED=true on its own — on every run it fetches http://SERVER_IP:WPAD_PORT/wpad.pac and writes the option wpad lines only on HTTP 200; otherwise it logs a WARNING, leaves them commented out and continues. This prevents every WPAD-aware client on the LAN from stalling on an unreachable PAC URL, a fault that raises no server-side error and only shows up as "the network is slow" everywhere at once. Check it yourself with curl -fsS --noproxy '*' --max-time 5 -o /dev/null "http://SERVER_IP:WPAD_PORT/wpad.pac"; echo $? — 0 means it will be activated. |
uhmleases.sh genera /etc/pydhcp/core/pydhcpd.conf dinámicamente en cada ejecución. Establezca WPAD_ENABLED=true en uhm.env para activar WPAD/PAC vía DHCP option 252, y WPAD_PORT al puerto en que escucha su VirtualHost de Apache (default 18100). Requisitos, que deben estar listos antes de poner true: Apache2 instalado, un VirtualHost escuchando en WPAD_PORT con ese puerto declarado en el ports.conf de Apache como Listen SERVER_IP:PORT, y un wpad.pac válido en su document root. Guarda: uhmleases.sh nunca confía en WPAD_ENABLED=true por sí solo — en cada ejecución descarga http://SERVER_IP:WPAD_PORT/wpad.pac y escribe las líneas option wpad solo si obtiene HTTP 200; si no, registra un WARNING, las deja comentadas y continúa. Esto evita que todos los clientes de la red que atienden WPAD se queden esperando una URL PAC inalcanzable, una avería que no genera ningún error en el servidor y que solo se manifiesta como "la red está lenta" en todas partes a la vez. Compruébelo con curl -fsS --noproxy '*' --max-time 5 -o /dev/null "http://SERVER_IP:WPAD_PORT/wpad.pac"; echo $? — un 0 significa que se activará. |
| WPAD/PAC scope | pydhcpd is ACL-agnostic — when WPAD_ENABLED=true it sends DHCP option 252 to every client, including mac-unlimited. Since unlimited devices must never go through the proxy, uhmiptables.sh blocks them from reaching port 18100 (the PAC file) at the firewall level; the PAC's own ; DIRECT fallback makes the browser proceed without a proxy for them. |
pydhcpd no distingue ACLs — cuando WPAD_ENABLED=true envía la opción DHCP 252 a todos los clientes, incluyendo mac-unlimited. Como los dispositivos unlimited nunca deben pasar por el proxy, uhmiptables.sh les bloquea el acceso al puerto 18100 (el archivo PAC) a nivel de firewall; el fallback ; DIRECT del propio PAC hace que el navegador siga sin proxy para ellos. |
| ping-check | ping-check true is enabled by default in the pydhcpd.conf generated by uhmleases.sh, along with ping-timeout (default 1s, controlled via PING_TIMEOUT_SECONDS in uhm.env). The daemon pings each IP before an OFFER to detect conflicts. In environments with strict ICMP firewall rules the ping will always time out silently and have no effect. Set PING_CHECK_ENABLED=false in uhm.env to disable it. |
ping-check true está activado por defecto en el pydhcpd.conf generado por uhmleases.sh, junto con ping-timeout (default 1s, controlado via PING_TIMEOUT_SECONDS en uhm.env). El demonio hace ping a cada IP antes del OFFER para detectar conflictos. En entornos con reglas de firewall estrictas que bloquean ICMP el ping siempre expirará sin efecto. Establezca PING_CHECK_ENABLED=false en uhm.env para desactivarlo. |
| Preventive guards | Checked unconditionally, every run, regardless of whether anything is actually wrong. Cheap when the scenario they guard against never happens (the normal case); their fallback behavior only activates if it does. Different in kind from reactive recovery (backup-config restore in uhmleases, the reload-failure backoff in uhmd) -- those only run after a failure is already detected, to recover from it. The guards below exist so a rare or unproven scenario degrades gracefully instead of cascading into a bigger failure (an aborted reload, a wrongly-promoted MAC, a silently corrupted ACL file). |
Se revisan sin condición, en cada corrida, sin importar si realmente hay algo mal. No cuestan nada cuando el escenario que protegen nunca ocurre (el caso normal); su comportamiento de fallback solo se activa si ocurre. Son de otra naturaleza que la recuperación reactiva (restauración de config de respaldo en uhmleases, el backoff por fallo de reload en uhmd) -- esas solo corren después de que ya se detectó un fallo, para recuperarse de él. Las guardas de abajo existen para que un escenario raro o no comprobado degrade con gracia en vez de encadenar una falla mayor (un reload abortado, una MAC promovida por error, un archivo ACL corrompido en silencio). |
| Voucher hostname length cap | process_sessions() (uhmd.sh) checks whether guestN-<voucher_code> would exceed 63 chars (the limit uhmleases.sh::_normalize_acl_file() enforces on uhm-auth.txt) before writing it. voucher_code comes from UniFi's API with no length guarantee from our side -- no known UniFi version has ever been observed returning one long enough to trigger this (real codes are short and numeric), but nothing rules it out for good. If it ever happened without this guard, the oversized line would abort normalization for the entire uhm-auth.txt file, not just that one client. With the guard, the voucher code is simply omitted from that one hostname (kept as plain guestN) and a WARNING is logged -- everything else proceeds normally. |
process_sessions() (uhmd.sh) revisa si guestN-<voucher_code> superaría los 63 caracteres (el límite que uhmleases.sh::_normalize_acl_file() exige en uhm-auth.txt) antes de escribirlo. voucher_code viene de la API de UniFi sin garantía de longitud de nuestro lado -- no se ha observado ninguna versión de UniFi que devuelva uno lo bastante largo como para disparar esto (los códigos reales son cortos y numéricos), pero nada lo descarta para siempre. Si pasara sin esta guarda, la línea de más de 63 caracteres abortaría la normalización de todo uhm-auth.txt, no solo la de ese cliente. Con la guarda, el código simplemente se omite de ese hostname puntual (queda como guestN plano) y se registra un WARNING -- todo lo demás sigue normal. |
is_managed_mac() live check |
Read fresh from disk on every call inside process_sessions/kick_newly_authorized/process_new_leases (uhmd.sh) -- guards against a stale or externally-granted UniFi guest session ever promoting a mac-*.txt device into uhm-auth.txt. In normal operation this never fires (managed devices don't go through the voucher flow at all); it only matters the day a residual session, a manual UniFi authorization, or a voucher redeemed before the device was added to mac-*.txt would otherwise slip through. |
Se lee en vivo del disco en cada llamada dentro de process_sessions/kick_newly_authorized/process_new_leases (uhmd.sh) -- protege contra que una sesión de invitado de UniFi residual o concedida por fuera alguna vez promueva a un dispositivo de mac-*.txt a uhm-auth.txt. En operación normal nunca se activa (los dispositivos gestionados ni pasan por el flujo de voucher); solo importa el día que una sesión residual, una autorización manual en UniFi, o un voucher canjeado antes de agregar el dispositivo a mac-*.txt se colarían si no estuviera. |
uhmwatch.sh reload-in-progress check |
_uhm_reload_in_progress() probes uhmd's cycle lock (non-blocking) before check_pydhcpd() declares the service OFFLINE. uhmleases.sh legitimately stops/reconfigures/starts pydhcpd for a few seconds on every real reload -- almost every cron tick (every minute) lands outside that window and never touches this guard's fallback path. It only matters the rare time a tick lands squarely inside it, where declaring OFFLINE and restarting would collide with uhmleases.sh's own pending restart and abort that reload. |
_uhm_reload_in_progress() prueba (sin bloquear) el lock de ciclo de uhmd antes de que check_pydhcpd() declare el servicio OFFLINE. uhmleases.sh legítimamente detiene/reconfigura/arranca pydhcpd por unos segundos en cada reload real -- casi todas las corridas de cron (cada minuto) caen fuera de esa ventana y nunca tocan el camino de fallback de esta guarda. Solo importa la rara vez que una corrida cae justo dentro, donde declarar OFFLINE y reiniciar chocaría con el restart que uhmleases.sh ya tenía pendiente y abortaría ese reload. |
mac-*.txt IP range conflict check |
check_mac_ip_ranges() (uhmleases.sh) validates, on every reload, that no admin-picked mac-*.txt IP falls inside UHM_INI_RANGE-UHM_END_RANGE or the block-pool range. Never fires as long as mac-*.txt IPs are chosen outside both ranges (the documented, expected setup); it only matters the day a typo or a copy-pasted IP lands inside one, where it aborts the reload with a specific ERROR: instead of silently corrupting DHCP behavior for both the conflicting device and whoever else was assigned that same range. |
check_mac_ip_ranges() (uhmleases.sh) valida, en cada reload, que ninguna IP de mac-*.txt elegida por el admin caiga dentro de UHM_INI_RANGE-UHM_END_RANGE ni del rango del pool de bloqueo. Nunca se activa mientras las IPs de mac-*.txt se elijan fuera de ambos rangos (la configuración esperada y documentada); solo importa el día que un typo o una IP copiada y pegada caiga dentro de uno, donde aborta el reload con un ERROR: puntual en vez de corromper en silencio el comportamiento DHCP tanto del dispositivo en conflicto como de quien más tuviera asignado ese mismo rango. |
| ACL file-swap count checks | clean_expired_macs() (uhmd.sh) and drain_lease_queue() (uhmleases.sh) both count entries before and after rewriting a file, and refuse to commit the swap (keep the original, log an ERROR) if the counts don't reconcile with what was actually expired/removed. Never fires when the rewrite logic behaves as expected (the normal case, every cycle); it only matters the day a parsing edge case would otherwise silently drop entries during a file rewrite. |
clean_expired_macs() (uhmd.sh) y drain_lease_queue() (uhmleases.sh) cuentan entradas antes y después de reescribir un archivo, y se niegan a confirmar el cambio (conservan el original, registran un ERROR) si los conteos no cuadran con lo que realmente se expiró/removió. Nunca se activa cuando la lógica de reescritura se comporta como se espera (el caso normal, en cada ciclo); solo importa el día que un caso límite de parseo, de no estar esto, descartaría entradas en silencio al reescribir un archivo. |
| These are platform and device limitations, not defects in this project. | Estas son limitaciones de plataforma y dispositivo, no defectos de este proyecto. |
| Limitation | Description | Limitación | Descripción |
|---|---|---|---|
| WPAD not supported | Android and iOS ignore DHCP option 252. The proxy must be configured manually on each device. | WPAD no compatible | Android e iOS ignoran la opción DHCP 252. El proxy debe configurarse manualmente en cada dispositivo. |
| Captive portal probes | Android probes connectivitycheck.gstatic.com; iOS probes captive.apple.com. If blocked or intercepted, the device reports "connected without internet" even when the proxy works. Whitelist these in Squid without auth. |
Sondas del portal cautivo | Android sondea connectivitycheck.gstatic.com; iOS sondea captive.apple.com. Si están bloqueados o interceptados, el dispositivo reporta "conectado sin internet" aunque el proxy funcione. Agréguelos a la whitelist de Squid sin autenticación. |
| App proxy bypass | Most apps on Android and iOS bypass the system proxy and connect directly. Only browsers reliably honor a manual proxy. Without SSL bump, direct HTTPS traffic cannot be redirected. | Apps que eluden el proxy | La mayoría de las aplicaciones de Android e iOS eluden el proxy del sistema y se conectan directamente. Solo los navegadores suelen respetar un proxy manual. Sin inspección SSL (SSL bump), el tráfico HTTPS directo no se puede redirigir. |
| MAC randomization | Android 10+ and iOS 14+ randomize the MAC per network by default. A randomized MAC will never match an ACL entry and will appear as unauthorized on every connection. Users must disable MAC randomization for the SSID before connecting. | Aleatorización de MAC | Android 10+ e iOS 14+ aleatorizan la MAC por red por defecto. Una MAC aleatorizada nunca coincidirá con una entrada ACL y aparecerá como no autorizada en cada conexión. El usuario debe deshabilitar la aleatorización de MAC para el SSID antes de conectarse. |
This behavior applies only to the optional proxy architecture described in uhmiptables_example.txt (iptables HTTP redirection to Squid, optionally using PAC via DHCP Option 252). It is not a defect in this project.
|
Este comportamiento aplica únicamente a la arquitectura opcional con proxy descrita en uhmiptables_example.txt (redirección HTTP mediante iptables hacia Squid, opcionalmente usando PAC mediante la Opción 252 de DHCP). No es un defecto de este proyecto.
|
| Limitation | Description | Limitación | Descripción |
|---|---|---|---|
| Windows NCSI probe | Windows periodically requests http://www.msftconnecttest.com/connecttest.txt to determine Internet connectivity. When HTTP traffic is transparently redirected to Squid (REDIRECT 80 → 3128), NCSI may receive an HTTP 404 response after successful voucher authentication. This does not affect normal Internet access. |
Sonda NCSI de Windows | Windows consulta periódicamente http://www.msftconnecttest.com/connecttest.txt para determinar la conectividad a Internet. Cuando el tráfico HTTP se redirige transparentemente hacia Squid (REDIRECT 80 → 3128), NCSI puede recibir una respuesta HTTP 404 después de una autenticación exitosa mediante voucher. Esto no afecta el acceso normal a Internet. |
| This is a structural limitation of MAC-based classification, not a code defect — see mitigation below. | Esta es una limitación estructural de la clasificación basada en MAC, no un defecto de código — ver mitigación abajo. |
| Limitation | Description | Limitación | Descripción |
|---|---|---|---|
mac-*.txt IP range is administrator-defined, not a config variable |
uhm.env only defines two IP ranges: UHM_INI_RANGE/UHM_END_RANGE for uhm-auth.txt, and SERV_INI_RANGE_BLOCK/SERV_END_RANGE_BLOCK for the pydhcp pool (uhm-grace.txt/blockdhcp.txt). mac-*.txt files (mac-limited.txt, mac-unlimited.txt) don't exist by default — uhmsetup.sh only creates the /etc/acl/mac directory; the administrator creates these files and picks their IPs manually, with no dedicated range enforced by uhm.env itself. uhmleases.sh's check_mac_ip_ranges() validates this on every run: any mac-*.txt IP landing inside either reserved range aborts the reload with a specific ERROR: log line — see uhmleases below for examples — but the safest practice is keeping every mac-*.txt IP outside both ranges from the start. |
El rango de IP de mac-*.txt es decisión del administrador, no una variable de configuración |
uhm.env solo define dos rangos de IP: UHM_INI_RANGE/UHM_END_RANGE para uhm-auth.txt, y SERV_INI_RANGE_BLOCK/SERV_END_RANGE_BLOCK para el pool de pydhcp (uhm-grace.txt/blockdhcp.txt). Los archivos mac-*.txt (mac-limited.txt, mac-unlimited.txt) no existen por defecto — uhmsetup.sh solo crea el directorio /etc/acl/mac; el administrador crea estos archivos y elige sus IPs manualmente, sin rango dedicado impuesto por uhm.env. check_mac_ip_ranges() en uhmleases.sh valida esto en cada corrida: cualquier IP de mac-*.txt que caiga dentro de alguno de los dos rangos reservados aborta el reload con una línea ERROR: puntual — ver uhmleases más abajo para ejemplos — pero lo más seguro es mantener siempre las IPs de mac-*.txt fuera de ambos rangos desde el principio. |
| Indefinite MAC rotation bypasses grace→block promotion | uhm-grace.txt classification is keyed exclusively by MAC address (see MAC randomization above). A client that presents a new MAC on each reconnection is treated as a brand-new client every time: it receives a fresh BLOCKDHCP_GRACE_SECONDS timer and never accumulates enough grace-period age to be promoted to blockdhcp.txt. pydhcpd's own DHCP rate-limiting (keyed per-MAC) does not mitigate this — it throttles request volume from a single identity, not the number of distinct identities a client can present, so the pattern is unaffected by any per-MAC threshold. DHCP client-hostname (option 12) cannot serve as a secondary identity signal either: it is client-supplied, unauthenticated (trivially spoofable), and not always present in pydhcpd.leases to begin with. There is no way to correlate rotated MACs to the same physical device from pydhcpd.leases alone; that would require device fingerprinting at the AP/802.11 layer, outside the scope of a DHCP-lease-based tool. Impact is bounded by firewall scope, not eliminated: the macgrace ipset only grants DNS resolution and captive-portal ports — the same access any new, first-time client already receives — so rotating a MAC indefinitely does not grant more network access than a single legitimate connection would, provided the macgrace DNS rule is restricted to the configured resolvers (SERV_DNS), as in the reference uhmiptables_example.txt. If that rule instead accepts DNS to any destination, grace-state clients gain an unrestricted DNS channel that can be used for DNS tunneling — combined with indefinite MAC rotation, this becomes a persistent internet bypass that never requires redeeming a voucher. The residual cost of MAC rotation even with the DNS rule restricted is operational, not a security bypass: uhm-grace.txt/blockdhcp.txt accumulate entries for MACs that are never reused, and each rotation consumes a DHCP pool lease. |
Rotación indefinida de MAC evade la promoción grace→block | La clasificación en uhm-grace.txt se basa exclusivamente en la dirección MAC (ver Aleatorización de MAC arriba). Un cliente que presenta una MAC nueva en cada reconexión es tratado como cliente completamente nuevo cada vez: recibe un temporizador BLOCKDHCP_GRACE_SECONDS fresco y nunca acumula suficiente antigüedad en gracia como para ser promovido a blockdhcp.txt. El propio rate-limiting DHCP de pydhcpd (por MAC) no mitiga esto — limita el volumen de solicitudes de una sola identidad, no la cantidad de identidades distintas que un cliente puede presentar, así que el patrón no se ve afectado por ningún umbral por-MAC. El hostname DHCP (opción 12) tampoco puede servir como señal secundaria de identidad: lo provee el cliente, no está autenticado (trivialmente falsificable), y ni siquiera está siempre presente en pydhcpd.leases. No hay forma de correlacionar MACs rotadas con el mismo dispositivo físico solo desde pydhcpd.leases; eso requeriría fingerprinting de dispositivo a nivel de AP/802.11, fuera del alcance de una herramienta basada en leases DHCP. El impacto está acotado por el alcance del firewall, no eliminado: el ipset macgrace solo otorga resolución DNS y los puertos del portal cautivo — el mismo acceso que ya recibe cualquier cliente nuevo de primera vez — así que rotar la MAC indefinidamente no otorga más acceso de red del que ya tendría una sola conexión legítima, siempre que la regla DNS de macgrace esté restringida a los resolvers configurados (SERV_DNS), como en el uhmiptables_example.txt de referencia. Si esa regla en cambio acepta DNS a cualquier destino, los clientes en estado grace ganan un canal DNS sin restricción utilizable para DNS tunneling — combinado con rotación indefinida de MAC, esto se convierte en un bypass de internet persistente que nunca requiere canjear un voucher. El costo residual de la rotación de MAC incluso con la regla DNS restringida es operativo, no un bypass de seguridad: uhm-grace.txt/blockdhcp.txt acumulan entradas de MACs que nunca se reutilizan, y cada rotación consume un lease del pool DHCP. |
| These are UniFi platform/API behaviors, not defects in this project. | Estos son comportamientos de la plataforma/API de UniFi, no defectos de este proyecto. |
| Limitation | Description | Limitación | Descripción |
|---|---|---|---|
stat/guest doesn't distinguish deleted vs. quota-exhausted vouchers |
When a voucher is deleted manually from the UniFi UI, stat/guest still retains session records tagged with that voucher_code, indistinguishable from a voucher whose quota simply ran out. This lets affected clients reconnect without re-entering a code. Reported to Ubiquiti: community.ui.com/31faff3e. Mitigated in uhmunifi.sh by Revoke by voucher code (action 4), which cleans stat/guest/stat/sta directly instead of relying on stat/voucher state. |
stat/guest no distingue vouchers eliminados de vouchers con cuota agotada |
Cuando un voucher se elimina manualmente desde la UI de UniFi, stat/guest sigue reteniendo registros de sesión con ese voucher_code, indistinguibles de un voucher cuya cuota simplemente se agotó. Esto permite que los clientes afectados se reconecten sin volver a ingresar un código. Reportado a Ubiquiti: community.ui.com/31faff3e. Mitigado en uhmunifi.sh mediante Revoke by voucher code (acción 4), que limpia stat/guest/stat/sta directamente sin depender del estado de stat/voucher. |
stat/voucher has no historical record of expired vouchers |
UniFi does not retain a voucher in stat/voucher once it expires or its quota is fully consumed; the entry disappears entirely instead of being marked expired. Verified directly against a live controller: five vouchers confirmed issued and consumed via /var/log/uhm.log (Authorized/Expired lines) returned zero matches when queried by code against stat/voucher after expiry. As a result, uhmunifi.sh's Vouchers section and Delete expired vouchers (action 3) can only ever act on what the controller still tracks at query time — they cannot produce a historical report of all vouchers ever issued. The only durable record of past voucher activity is /var/log/uhm.log. |
stat/voucher no tiene registro histórico de vouchers expirados |
UniFi no retiene un voucher en stat/voucher una vez que expira o su cuota se consume por completo; la entrada desaparece por completo en vez de marcarse como expirada. Verificado directamente contra un controlador en vivo: cinco vouchers confirmados como emitidos y consumidos vía /var/log/uhm.log (líneas Authorized/Expired) devolvieron cero coincidencias al consultarlos por código contra stat/voucher después de expirar. Como consecuencia, la sección Vouchers de uhmunifi.sh y Delete expired vouchers (acción 3) solo pueden actuar sobre lo que el controlador todavía rastrea al momento de la consulta — no pueden producir un reporte histórico de todos los vouchers emitidos alguna vez. El único registro duradero de actividad histórica de vouchers es /var/log/uhm.log. |
kick-sta can fail with HTTP 400 right after a successful authorization |
The voucher redemption itself always succeeds independently of this: the client is already promoted to uhm-auth.txt with its fixed hotspot IP in step 7 (sessions), well before kick_newly_authorized() runs in step 10. The kick-sta call is a best-effort convenience against the UniFi API (cmd/stamgr) to force the client to re-associate immediately with its new IP; if UniFi rejects that specific request with HTTP 400 (typically a race between the just-granted authorization and what stat/sta still reports for that MAC at that instant), the client simply keeps its old pool-range IP until its own DHCP renewal timer fires, and the client-facing symptom can be an HTTP 400/404 from UniFi's own captive-portal web layer while the browser tries to continue on the stale IP — a separate HTTP exchange from the kick-sta call, on a different endpoint, that just happens to surface around the same time. Nothing in this project's ACLs or firewall rules is at fault; the log line is written by kick_newly_authorized() itself, not by uhmleases.sh/uhmiptables.sh. Example from /var/log/uhm.log: INFO: failed to kick 02:00:00:aa:bb:20 (HTTP 400) -- skip. The current code only logs the HTTP status code, not UniFi's response body, so the controller's exact rejection reason isn't recoverable from uhm.log alone. |
kick-sta puede fallar con HTTP 400 justo después de una autorización exitosa |
La redención del voucher en sí siempre tiene éxito de forma independiente a esto: el cliente ya quedó promovido a uhm-auth.txt con su IP fija de hotspot en el paso 7 (sessions), mucho antes de que kick_newly_authorized() se ejecute en el paso 10. La llamada a kick-sta es un intento de conveniencia (best-effort) contra la API de UniFi (cmd/stamgr) para forzar al cliente a reasociarse de inmediato con su nueva IP; si UniFi rechaza esa petición puntual con HTTP 400 (típicamente una condición de carrera entre la autorización recién otorgada y lo que stat/sta todavía reporta para ese MAC en ese instante), el cliente simplemente conserva su IP vieja del rango de pool hasta que su propio temporizador de renovación DHCP se cumpla, y el síntoma visible para el cliente puede ser un HTTP 400/404 de la propia capa web del portal cautivo de UniFi mientras el navegador intenta continuar con la IP vieja — un intercambio HTTP distinto al de kick-sta, sobre un endpoint diferente, que solo coincide en el tiempo. No hay ninguna falla en las ACLs ni en las reglas de firewall de este proyecto; la línea de log la escribe el propio kick_newly_authorized(), no uhmleases.sh/uhmiptables.sh. Ejemplo de /var/log/uhm.log: INFO: failed to kick 02:00:00:aa:bb:20 (HTTP 400) -- skip. El código actual solo registra el código HTTP, no el cuerpo de la respuesta de UniFi, así que el motivo exacto del rechazo del controlador no se puede recuperar solo con uhm.log. |
Both unifi-os and classic run MongoDB embedded (container, or subprocess of unifi.service on port 27117). The standalone mongod.service in classic is disabled by default. The issue below only occurs if that instance is shared with another application.
|
Tanto unifi-os como classic ejecutan MongoDB embebido (contenedor, o subproceso de unifi.service en el puerto 27117). La unidad independiente mongod.service de classic está deshabilitada por defecto. El problema descrito a continuación solo puede ocurrir si esa instancia se comparte con otra aplicación.
|
| Issue | Description | Problema | Descripción |
|---|---|---|---|
| MongoDB cannot write to its data directory | Clients cannot reach the captive portal. MongoDB logs (sudo journalctl -u mongod -f) show code=dumped, status=6/ABRT, code=exited, or status=14/n/a, indicating that MongoDB cannot write to its data directory.Fix: systemctl stop mongodchown mongodb:mongodb /var/lib/mongodb/WiredTiger.turtlechown mongodb:mongodb /var/lib/mongodb/WiredTiger.wtchown -R mongodb:mongodb /var/lib/mongodbsystemctl start mongodVerify: sudo systemctl status mongod |
MongoDB no puede escribir en su directorio de datos | Los clientes no pueden acceder al portal cautivo. Los registros de MongoDB (sudo journalctl -u mongod -f) muestran code=dumped, status=6/ABRT, code=exited o status=14/n/a, indicando que MongoDB no puede escribir en su directorio de datos.Solución: systemctl stop mongodchown mongodb:mongodb /var/lib/mongodb/WiredTiger.turtlechown mongodb:mongodb /var/lib/mongodb/WiredTiger.wtchown -R mongodb:mongodb /var/lib/mongodbsystemctl start mongodVerificar: sudo systemctl status mongod |
| This project is designed to run locally and be accessed over a LAN. It is not recommended to expose it to the internet, as it lacks the hardening required for public-facing deployments. If you choose to publish it despite this warning, it is strongly recommended to do so through an on-demand tunnel rather than opening ports directly. This approach lets you start and stop public access at will, without permanently exposing your server. | Este proyecto está diseñado para ejecutarse localmente y ser accedido en red LAN. No se recomienda exponerlo a internet, ya que no cuenta con el endurecimiento necesario para despliegues públicos. Si decide publicarlo a pesar de esta advertencia, se recomienda hacerlo a través de un túnel bajo demanda en lugar de abrir puertos directamente. Este enfoque le permite iniciar y detener el acceso público a voluntad, sin exponer el servidor de forma permanente. |
Optional tunnel:
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.


















