Router DHCP con dnsmasq

Concepto

Un contenedor Docker con dnsmasq actúa como servidor DHCP en vmbr1 (ver Configuración de red de Proxmox). Cada vez que se crea o arranca una VM, un hookscript de Proxmox lee automáticamente el VMID y la MAC de la VM y registra la reserva DHCP sin intervención manual. Al borrar la VM, la reserva se elimina.

Esquema de IPs: 10.10.10.<VMID> — el VMID debe ser ≤ 254.

Arquitectura

VM arranca (VMID=105)
      ↓
hookscript lee MAC de /etc/pve/qemu-server/105.conf
      ↓
calcula IP → 10.10.10.105
      ↓
escribe en /opt/stack/dnsmasq/hosts.d/reservas.conf:
  BC:24:11:AA:BB:CC,10.10.10.105
      ↓
docker kill --signal=HUP dnsmasq-router → recarga sin reiniciar

Ficheros implicados

FicheroUbicación en hostDescripción
docker-compose.yml/opt/stack/Despliegue del contenedor (junto con NginxPM)
dnsmasq.conf/opt/stack/dnsmasq/config/Configuración del servidor DHCP
reservas.conf/opt/stack/dnsmasq/hosts.d/Reservas dinámicas VMID↔MAC↔IP
hookscript.sh/var/lib/vz/snippets/Script ejecutado por Proxmox en cada evento de VM

Configuración de dnsmasq

Crear /opt/stack/dnsmasq/config/dnsmasq.conf:

# Interfaz donde escucha (red interna de VMs)
interface=vmbr1
bind-interfaces

# Rango DHCP dinámico (para IPs sin reserva)
dhcp-range=10.10.10.100,10.10.10.200,12h

# Fichero de leases
dhcp-leasefile=/var/lib/dnsmasq/leases

# Fichero de reservas estáticas (VMID→MAC→IP)
dhcp-hostsfile=/etc/dnsmasq/hosts.d/reservas.conf

# DNS upstream
server=8.8.8.8
server=8.8.4.4

# No leer /etc/hosts del contenedor
no-hosts

# Log
log-dhcp

Crear /opt/stack/dnsmasq/hosts.d/reservas.conf vacío:

touch /opt/stack/dnsmasq/hosts.d/reservas.conf

Hookscript

Crear /var/lib/vz/snippets/hookscript.sh:

#!/bin/bash
#
# Hookscript de Proxmox — registra/elimina reservas DHCP en dnsmasq
# Uso: asignado a cada VM con: qm set <VMID> --hookscript local:snippets/hookscript.sh
#

VMID=$1
PHASE=$2

RESERVAS_FILE="/opt/stack/dnsmasq/hosts.d/reservas.conf"
DNSMASQ_CONTAINER="dnsmasq-router"
IP="10.10.10.${VMID}"

# Lee la MAC de la interfaz net0 del config de la VM
get_mac() {
    grep "^net0:" /etc/pve/qemu-server/${VMID}.conf 2>/dev/null \
        | grep -oP '([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}' \
        | head -1
}

reload_dnsmasq() {
    docker kill --signal=HUP "$DNSMASQ_CONTAINER" > /dev/null 2>&1
}

case "$PHASE" in
    pre-start)
        MAC=$(get_mac)
        if [ -z "$MAC" ]; then
            echo "hookscript: VMID ${VMID} no tiene net0, saltando."
            exit 0
        fi

        # Eliminar entradas previas para esta IP o MAC (evita duplicados)
        sed -i "/,${IP}$/d" "$RESERVAS_FILE"
        sed -i "/^${MAC},/d" "$RESERVAS_FILE"

        # Añadir nueva reserva
        echo "${MAC},${IP}" >> "$RESERVAS_FILE"
        echo "hookscript: registrada ${MAC} → ${IP}"

        reload_dnsmasq
        ;;

    post-stop)
        MAC=$(get_mac)
        if [ -z "$MAC" ]; then
            exit 0
        fi

        # Eliminar reserva
        sed -i "/^${MAC},/d" "$RESERVAS_FILE"
        sed -i "/,${IP}$/d" "$RESERVAS_FILE"
        echo "hookscript: eliminada reserva ${MAC} → ${IP}"

        reload_dnsmasq
        ;;
esac

exit 0

Dar permisos de ejecución:

chmod +x /var/lib/vz/snippets/hookscript.sh

Activar el hookscript en una VM

El hookscript se asigna automáticamente al crear el template con create-haos-template.sh (ver Máquinas Virtuales y Contenedores). Para asignarlo manualmente a una VM existente:

qm set <VMID> --hookscript local:snippets/hookscript.sh

Eventos del hookscript

EventoAcción
pre-startLee MAC, calcula IP, deduplica y escribe reserva, recarga dnsmasq
post-stopElimina reserva del fichero, recarga dnsmasq