Exploring the roots and future of Internet Relay Chat

systemd para servidores IRC: units, sandboxing y gestión de servicios

Cuando empecé a administrar servidores IRC, los servicios se lanzaban con scripts de init.d escritos a mano, screen sessions que nadie cerraba, y nohup en el mejor de los casos. Recuerdo reinicios de servidor donde había que acordarse de lanzar manualmente el IRCd, después Anope, después el eggdrop, todo en el orden correcto. Si te olvidabas del orden, Anope intentaba conectar antes de que el IRCd estuviera listo y entraba en un bucle de reconexión.

systemd resuelve todo esto. Definición declarativa de servicios, dependencias explícitas, restart automático, sandboxing de seguridad, y logging centralizado. Si todavía lanzas tu IRCd con un script de shell o desde screen, este artículo es para ti.

¿Por qué systemd para IRC?

Las razones concretas:

  • Arranque automático — El IRCd arranca con el sistema. Sin intervención manual.
  • Restart en fallo — Si el proceso muere (crash, OOM, lo que sea), systemd lo reinicia automáticamente.
  • Dependencias — Anope no arranca hasta que el IRCd esté ready. ZNC no arranca hasta que la red esté disponible.
  • Sandboxing — Puedes restringir qué puede hacer cada servicio: sin acceso a /home, sin capacidad de montar filesystems, sin acceso a red excepto lo necesario.
  • Logging unificado — journalctl en vez de buscar en /var/log/ircd/ o peor, en algún directorio random del home del usuario que compiló el IRCd.
  • Gestión de recursos — Limitar CPU y RAM por servicio con cgroups.

Unit para el IRCd

UnrealIRCd

# /etc/systemd/system/unrealircd.service
[Unit]
Description=UnrealIRCd IRC Server
After=network-online.target
Wants=network-online.target
Documentation=https://www.unrealircd.org/docs/

[Service]
Type=forking
User=ircd
Group=ircd
WorkingDirectory=/home/ircd/unrealircd
ExecStart=/home/ircd/unrealircd/bin/unrealircd
ExecReload=/bin/kill -HUP $MAINPID
PIDFile=/home/ircd/unrealircd/data/unrealircd.pid
Restart=on-failure
RestartSec=10
TimeoutStartSec=30
TimeoutStopSec=30

# Hardening básico
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/home/ircd/unrealircd/data /home/ircd/unrealircd/logs /home/ircd/unrealircd/tmp
PrivateTmp=true

[Install]
WantedBy=multi-user.target

InspIRCd

# /etc/systemd/system/inspircd.service
[Unit]
Description=InspIRCd IRC Server
After=network-online.target
Wants=network-online.target

[Service]
Type=forking
User=ircd
Group=ircd
WorkingDirectory=/home/ircd/inspircd
ExecStart=/home/ircd/inspircd/bin/inspircd start
ExecReload=/home/ircd/inspircd/bin/inspircd rehash
ExecStop=/home/ircd/inspircd/bin/inspircd stop
PIDFile=/home/ircd/inspircd/data/inspircd.pid
Restart=on-failure
RestartSec=10

# InspIRCd también soporta Type=simple si lo compilas
# con --disable-fork. En ese caso:
# Type=simple
# ExecStart=/home/ircd/inspircd/bin/inspircd --nofork

NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/home/ircd/inspircd/data /home/ircd/inspircd/logs
PrivateTmp=true

[Install]
WantedBy=multi-user.target
Consejo: Usa Type=forking con PIDFile= si tu IRCd se daemoniza (comportamiento por defecto). Si puedes ejecutarlo en foreground (con flags como --nofork o -F), usa Type=simple — es más fiable porque systemd no necesita rastrear el PID file. UnrealIRCd 6.x soporta foreground con -F.

Unit para servicios (Anope/Atheme)

Anope

# /etc/systemd/system/anope.service
[Unit]
Description=Anope IRC Services
After=unrealircd.service
Requires=unrealircd.service
Documentation=https://wiki.anope.org/

[Service]
Type=forking
User=ircd
Group=ircd
WorkingDirectory=/home/ircd/anope
ExecStart=/home/ircd/anope/bin/anope
ExecReload=/bin/kill -HUP $MAINPID
PIDFile=/home/ircd/anope/data/anope.pid
Restart=on-failure
RestartSec=15

# Esperar un poco más que el IRCd para que el link esté listo
ExecStartPre=/bin/sleep 5

NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/home/ircd/anope/data /home/ircd/anope/logs /home/ircd/anope/backups
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Atheme

# /etc/systemd/system/atheme.service
[Unit]
Description=Atheme IRC Services
After=unrealircd.service
Requires=unrealircd.service

[Service]
Type=forking
User=ircd
Group=ircd
WorkingDirectory=/home/ircd/atheme
ExecStart=/home/ircd/atheme/bin/atheme-services
PIDFile=/home/ircd/atheme/var/atheme.pid
Restart=on-failure
RestartSec=15
ExecStartPre=/bin/sleep 5

NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/home/ircd/atheme/var /home/ircd/atheme/etc
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Fíjate en las directivas After= y Requires=. Esto le dice a systemd: "no arranques Anope/Atheme hasta que UnrealIRCd haya arrancado, y si UnrealIRCd se para, para Anope también". El ExecStartPre=/bin/sleep 5 es un hack necesario — el IRCd puede estar "arrancado" según systemd pero el listener aún no está listo. 5 segundos es suficiente en la mayoría de casos.

Unit para ZNC

# /etc/systemd/system/znc.service
[Unit]
Description=ZNC IRC Bouncer
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=znc
Group=znc
ExecStart=/usr/bin/znc --foreground --datadir /var/lib/znc
Restart=on-failure
RestartSec=10

# ZNC soporta --foreground nativo, así que usamos Type=simple
# Es más limpio que Type=forking

NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/znc
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true

# Límites de recursos
LimitNOFILE=8192

[Install]
WantedBy=multi-user.target

ZNC es el caso más sencillo porque soporta foreground nativamente y no depende de ningún otro servicio local (conecta a IRCds remotos).

Sandboxing y hardening

Esta es la parte que más valor aporta systemd sobre un script de init clásico. Puedes restringir severamente qué puede hacer cada servicio:

# Hardening agresivo para un IRCd
# Añadir a la sección [Service] de cualquier unit:

# === Filesystem ===
ProtectSystem=strict             # / es read-only
ProtectHome=true                 # /home es inaccesible
ReadWritePaths=/home/ircd/unrealircd/data /home/ircd/unrealircd/logs
PrivateTmp=true                  # /tmp aislado por servicio
ProtectKernelTunables=true       # /proc/sys read-only
ProtectKernelModules=true        # No cargar módulos kernel
ProtectControlGroups=true        # /sys/fs/cgroup read-only

# === Privilegios ===
NoNewPrivileges=true             # No puede escalar privilegios
CapabilityBoundingSet=CAP_NET_BIND_SERVICE  # Solo bind a puertos
AmbientCapabilities=CAP_NET_BIND_SERVICE    # Bind a puertos <1024
# (necesario si usas puerto 6667)

# === Red ===
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
# Solo IPv4, IPv6 y sockets Unix

# === System calls ===
SystemCallFilter=@system-service
SystemCallArchitectures=native
# Restringe a syscalls de servicio normales
# Bloquea syscalls de debugging, módulos kernel, etc.

# === Otros ===
MemoryDenyWriteExecute=true      # No W^X (JIT imposible)
LockPersonality=true
RestrictRealtime=true
RestrictSUIDSGID=true
RemoveIPC=true
Advertencia: MemoryDenyWriteExecute=true puede causar problemas si tu IRCd o módulos usan JIT o mapean memoria como ejecutable. Atheme con ciertos módulos de scripting puede fallar con esta opción. Prueba siempre el hardening completo en staging antes de aplicarlo en producción. Activa las opciones una por una y verifica que el servicio sigue funcionando.

Para verificar el nivel de hardening de tu unit:

# systemd-analyze security te da un score de 0 (inseguro) a 10 (blindado)
sudo systemd-analyze security unrealircd.service

# Output ejemplo:
# → Overall exposure level for unrealircd.service: 4.2 OK
# NAME                          DESCRIPTION                 EXPOSURE
# PrivateNetwork=               Service has access to host  0.5
# ProtectSystem=                Service has full access...  ...
# ...

# Score ideal: < 5.0
# Sin hardening: suele estar en 8-9 (muy expuesto)

Dependencias y orden de arranque

El orden de arranque típico de una infraestructura IRC es:

# Secuencia de arranque:
# 1. Red disponible (network-online.target)
# 2. IRCd arranca y escucha en puertos
# 3. Servicios (Anope/Atheme) conectan al IRCd
# 4. ZNC (si es local) conecta al IRCd
# 5. Bots conectan

# Diagrama de dependencias systemd:
#
# network-online.target
#    ├── unrealircd.service
#    │      ├── anope.service (After + Requires)
#    │      └── eggdrop.service (After + Wants)
#    └── znc.service (independiente)

# Si quieres un target custom para agrupar todo:
# /etc/systemd/system/irc.target
[Unit]
Description=IRC Infrastructure
Requires=unrealircd.service anope.service
Wants=znc.service
After=unrealircd.service anope.service znc.service

[Install]
WantedBy=multi-user.target

# Ahora puedes arrancar/parar todo con:
sudo systemctl start irc.target
sudo systemctl stop irc.target

Diferencia entre Requires y Wants

Requires= es estricto: si el IRCd no arranca, Anope tampoco arranca. Si el IRCd se para, Anope se para. Wants= es suave: si el IRCd no arranca, systemd intenta arrancar Anope de todos modos (fallará, pero no es un error de systemd). Usa Requires para dependencias estrictas (servicios que literalmente no funcionan sin el IRCd) y Wants para dependencias opcionales (bots que pueden reconectar solos).

Logging con journald

systemd captura stdout y stderr de los servicios en journald. Esto te da logging centralizado con filtros por servicio, timestamps precisos y rotación automática.

# Ver logs del IRCd en tiempo real:
journalctl -u unrealircd.service -f

# Ver logs de los últimos 30 minutos:
journalctl -u unrealircd.service --since "30 min ago"

# Ver logs de todos los servicios IRC:
journalctl -u unrealircd.service -u anope.service -u znc.service

# Filtrar por prioridad (errores y warnings):
journalctl -u unrealircd.service -p warning

# Logs entre fechas:
journalctl -u unrealircd.service --since "2024-05-15" --until "2024-05-16"

# Exportar a archivo:
journalctl -u unrealircd.service --since today > /tmp/ircd-today.log

# Configurar retención en /etc/systemd/journald.conf:
# SystemMaxUse=500M      # Máximo 500 MB de logs totales
# MaxRetentionSec=30d    # Máximo 30 días de retención

Un truco que uso: crear un alias para monitorizar toda la infraestructura IRC en un solo terminal:

# En ~/.bashrc o ~/.zshrc:
alias irc-logs='journalctl -u unrealircd -u anope -u znc -f --no-hostname'
alias irc-status='systemctl status unrealircd anope znc --no-pager'
alias irc-restart='sudo systemctl restart unrealircd && sleep 5 && sudo systemctl restart anope'

Preguntas frecuentes

¿Puedo usar systemd si compilo el IRCd desde source como usuario no-root?

Sí. La unit de systemd corre el servicio como el usuario que especifiques en User=. Puedes compilar el IRCd como usuario "ircd" en /home/ircd/ y la unit lo ejecutará con ese usuario. No necesitas ser root para compilar ni ejecutar — solo necesitas root (o sudo) para instalar la unit en /etc/systemd/system/ y para ejecutar systemctl enable/start.

¿El ExecStartPre sleep es la mejor forma de esperar al IRCd?

Es la más simple, no la mejor. La forma correcta sería usar Type=notify donde el IRCd envía una notificación a systemd cuando está listo (sd_notify). Pero ni UnrealIRCd ni InspIRCd soportan sd_notify nativamente. Otra opción es un script wrapper que haga poll al puerto del IRCd hasta que responda: ExecStartPre=/bin/bash -c 'until ss -tlnp | grep -q :6697; do sleep 1; done'. Pero honestamente, el sleep de 5 segundos funciona el 99% de las veces y es mucho más simple.

¿Puedo limitar la RAM que usa el IRCd con systemd?

Sí, con las directivas de cgroups. Añade MemoryMax=512M en la sección [Service] para limitar a 512 MB. Si el proceso intenta usar más, el OOM killer de cgroup lo matará y systemd lo reiniciará. También puedes usar MemoryHigh=400M como soft limit — el kernel reducirá la prioridad de asignación de memoria antes de llegar al hard limit. Para CPU: CPUQuota=50% limita a medio core.