Ayer el túnel funcionaba, y esta mañana la unit está muerta con Failed at step EXEC spawning /usr/local/bin/vpnclient en el journal. Las credenciales, las rutas y la cuenta nunca entran en esa frase. systemd resolvió ExecStart a una ruta, pidió al núcleo que la ejecutara, y el núcleo se negó, casi siempre porque no existe nada en esa ruta o porque el archivo no es ejecutable. Trata el fallo como un problema de archivo y argv hasta que el comando analizado demuestre lo contrario.

Por qué EXEC falla antes de que empiece la autenticación

systemd no ejecuta ExecStart a través de un shell. Hace un fork, construye un vector de argumentos a partir de la línea y llama a execve sobre el primer elemento. Si esa llamada falla, el servicio nunca llega al código propio del cliente de VPN, así que ninguna edición de perfil ni comprobación de credenciales puede ayudar.

El código de estado separa esta familia de las caídas ordinarias. Una unit que sale con status=203/EXEC nunca inició un proceso; una unit que arranca y luego sale con status=1 puede haber fallado en la autenticación, el DNS o el enrutamiento. Lee el código antes del registro del cliente, porque en el primer caso el registro del cliente estará vacío.

La ruta del journal es la que usó systemd, no necesariamente la que editaste. Los archivos drop-in y la salida del generador pueden sustituir el archivo del proveedor, y un daemon-reload obsoleto deja la línea antigua en memoria. Por eso el primer diagnóstico es preguntar a systemd qué cree que es el comando, en lugar de abrir el archivo que recuerdas haber cambiado.

Tres lugares donde se escribe la ruta del binario

Un cliente de VPN instalado desde un paquete de distribución, un script del proveedor y un tarball de versión pueden acabar en tres prefijos distintos. La unit, la regla de privilegios y el instalador registraron cada uno uno de esos prefijos, y el fallo aparece cuando dos de los tres no coinciden.

RecordCommon valueHow to read it
Unit or drop-in/usr/local/bin/vpnclientsystemctl cat vpnclient.service
Installed binary/usr/bin/vpnclientcommand -v vpnclient, then ls -l the result
Sudoers grant/opt/vpnclient/bin/vpnclientsudo grep -r vpnclient /etc/sudoers.d/

Una discrepancia concreta se ve así: el script del proveedor instala en /usr/local/bin, el tarball se descomprimió bajo /opt, y la unit sigue nombrando la ruta antigua /usr/bin de un paquete anterior. Solo existe uno de esos archivos, y rara vez es el que está en ExecStart.

Decide qué ruta es canónica antes de cambiar cualquiera de los archivos. Si el paquete de distribución es dueño del binario, la ruta del paquete es la canónica y la unit debe seguirla. Si lo es el script del proveedor, la misma regla se aplica al prefijo del proveedor. Copiar un binario entre prefijos crea dos copias que se separan en la próxima actualización.

Cuando un comando entre comillas se convierte en un nombre de archivo

Un segundo modo de fallo se esconde tras la misma línea del journal. Alguien escribe esto en la unit y espera que el cliente se conecte durante el arranque:

ExecStart=/usr/bin/"vpnclient connect office"

systemd divide el valor en palabras, así que la primera palabra se convierte en /usr/bin/vpnclient connect office al eliminar las comillas como sintaxis de agrupación. Luego el núcleo busca un archivo cuyo nombre contenga espacios, y no existe. El error es idéntico al de un binario ausente aunque el binario esté instalado.

La forma admitida es un ejecutable real seguido de argumentos separados, por ejemplo ExecStart=/usr/bin/vpnclient connect office. Si el flujo de trabajo necesita de verdad una tubería o una cadena, ponlo en un script y llama al script, porque ExecStart no tiene &&, ni tuberías, ni expansión de glob.

Arregla la unit, no el sistema de archivos

La reparación tentadora es un enlace simbólico desde la ruta esperada hasta el binario real. Funciona hasta que el paquete se actualiza y el enlace apunta a un archivo eliminado, o hasta que una etiqueta de SELinux deja de coincidir con el objetivo. Es preferible editar la unit mediante un drop-in, para que el archivo del proveedor quede intacto y las actualizaciones sigan siendo previsibles.

La regla de privilegios debe coincidir con la misma ruta y los mismos subcomandos. Una entrada de sudoers para /usr/local/bin/vpnclient no autoriza /usr/bin/vpnclient, y una entrada que permite cualquier argumento deja que cualquier proceso local reconfigure o detenga el túnel. Concede los dos verbos que necesita la unit y mantén el desmontaje fuera de ella si detener el servicio ya cierra la interfaz.

Aquí es donde la comparación vale la pena: la unit, la base de datos de paquetes y sudoers deberían nombrar una sola ruta. Cuando lo hacen, el error EXEC desaparece sin reiniciar y sin una copia del binario en un tercer lugar.

Demuestra el comando analizado antes de reiniciar

sudo systemctl daemon-reload
sudo systemctl cat vpnclient.service
systemctl show -p ExecStart vpnclient.service
systemd-analyze verify /etc/systemd/system/vpnclient.service
sudo systemctl restart vpnclient.service
systemctl status vpnclient.service --no-pager

systemctl show es la salida decisiva porque imprime el argv que systemd analizó realmente, incluido cualquier drop-in. systemd-analyze verify detecta una ruta ausente o no ejecutable antes de que reinicies, y el comando de estado tras el reinicio te dice si el proceso existe ahora.

Si la unit arranca y luego falla, has dejado la familia EXEC y has entrado en el manejo de errores propio del cliente. Busca en el journal el mensaje del cliente, no otro fallo de execve. Guarda el diff de la unit y el diff de sudoers juntos en el registro de cambios, para que la próxima actualización no reintroduzca en silencio la ruta dividida.

Artículos relacionados