Run Scripts on networkd-dispatcher State Changes
networkd-dispatcher turns a systemd-networkd state change into a shell script you control, no polling or cron jobs required. You will finish with a root-owned hook that fires the moment an interface goes routable, plus a way to check the service, its environment file and its startup behaviour. The examples use networkd-dispatcher 2.2.4-1, the version installed here.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about 20 minutes. You need a system running systemd-networkd, a shell and sudo access. The example only writes to the journal: it does not touch network configuration, but enabling the service can fire startup triggers immediately, so read the script before you install it.
1. Check What's Actually Installed
Start with the boring, read-only checks. They cost nothing and save you debugging the wrong binary:
$ command -v networkd-dispatcher
/usr/bin/networkd-dispatcher
$ dpkg-query -W -f='${Package} ${Version}\n' networkd-dispatcher
networkd-dispatcher 2.2.4-1
$ networkd-dispatcher --help
usage: networkd-dispatcher [-h] [-S SCRIPT_DIR] [-T] [-v] [-q]
The daemon listens for systemd-networkd signals over D-Bus, it does not poll. Its default search path is /etc/networkd-dispatcher:/usr/lib/networkd-dispatcher, and when a script of the same name exists in both, the one in /etc wins, which is exactly where your local hooks belong.
Checkpoint
If your version or path differs, trust your own --help output over this guide.
2. Pick the Right State Directory
Each hook lives in a directory named for the state it should fire on: routable.d, dormant.d, no-carrier.d, off.d, carrier.d, degraded.d, configured.d and configuring.d. Routable is the obvious one to start with, it fires once networkd reports the interface has usable routing.
These are not a generic watch-folder. A script in routable.d only runs on that specific state event, and the state names come from systemd-networkd itself, not administrative states such as failed or unmanaged.
Creating the directory only touches the filesystem, but it still needs root:
$ sudo install -d -o root -g root -m 0755 /etc/networkd-dispatcher/routable.d
$ stat -c '%U:%G %a %n' /etc/networkd-dispatcher/routable.d
root:root 755 /etc/networkd-dispatcher/routable.d
3. Install a Harmless Test Hook
Write a script that just logs the interface, state and address, using logger so you don't need to manage a log file yourself:
$ cat > /tmp/50-networkd-dispatcher-example <<'EOF'
#!/bin/sh
set -eu
/usr/bin/logger -t networkd-dispatcher \
"iface=${IFACE:-unknown} state=${STATE:-unknown} ipv4=${ADDR:-none}"
EOF
$ sudo install -o root -g root -m 0755 \
/tmp/50-networkd-dispatcher-example \
/etc/networkd-dispatcher/routable.d/50-example
$ stat -c '%U:%G %a %n' /etc/networkd-dispatcher/routable.d/50-example
root:root 755 /etc/networkd-dispatcher/routable.d/50-example
Owner, group and mode are not decoration. The daemon runs as root and, per the installed project documentation, ignores anything that is not root-owned and executable. The :- defaults keep the log line useful even when an event skips a variable, and IP_ADDRS and IP6_ADDRS can hold several space-separated addresses, so never assume there is only one.
Scripts in a state directory run in alphanumeric filename order, hence the 50- prefix. Keep hooks short and idempotent: a state transition can fire again after a reconnect or a restart.
4. Check the Service Before You Touch It
networkd-dispatcher ships its own unit. Look at the command and the optional environment file before changing anything:
$ systemctl cat networkd-dispatcher.service
[Service]
Type=notify
ExecStart=/usr/bin/networkd-dispatcher $networkd_dispatcher_args
EnvironmentFile=-/etc/default/%p
$ sed -n '1,80p' /etc/default/networkd-dispatcher
networkd_dispatcher_args="--run-startup-triggers"
--run-startup-triggers replays events for state that already existed when the daemon started, which is handy for catching an interface that came up before the dispatcher did. It also means your new hook can run the instant the service starts. Treat that as a service-disrupting boundary for any hook that restarts daemons, changes routes or starts a VPN; the example here only writes to the journal.
5. Start It and Watch the Journal
Starting the service is a privileged action, and it may run your hook immediately because of that startup trigger:
$ sudo systemctl enable --now networkd-dispatcher.service
$ systemctl is-enabled networkd-dispatcher.service
enabled
$ systemctl is-active networkd-dispatcher.service
active
$ systemctl status --no-pager networkd-dispatcher.service
You are looking for an active service; the exact PID and timestamps will differ. If it is not active, check the log before touching the script:
$ journalctl -u networkd-dispatcher.service -b --no-pager
$ journalctl -t networkd-dispatcher -b --no-pager
Sep 25 10:15:00 host networkd-dispatcher[1234]: iface=enp1s0 state=routable ipv4=192.0.2.10
Timestamp, host, PID and interface are all just examples. What matters is a dispatcher service with no start failure and a log line from your hook. If your interface is not going routable right now, do not force a network change to manufacture a test event, just check with networkctl status and wait for a normal reconnect or boot.
6. Know What the Flags Actually Do
The service reads extra arguments from /etc/default/networkd-dispatcher. The command line is deliberately small:
-Tor--run-startup-triggers. Emits events for state that already exists.-S SCRIPT_DIRor--script-dir SCRIPT_DIR. Replaces the script search path; separate multiple directories with a colon.-vand-q. Raise or lower logging verbosity by one level each.
Do not reach for -S just because a hook seems to be missing. It replaces where every hook is searched for and can hide the packaged directories entirely. Check spelling, ownership, the executable bit and the state directory first. If you do edit the defaults file, restart during a maintenance window and check systemctl cat again afterwards.
7. Roll Back Cleanly
To undo this guide, remove only the hook you created. Restarting afterwards just clears the service's in-memory view:
$ sudo rm -- /etc/networkd-dispatcher/routable.d/50-example
$ sudo systemctl restart networkd-dispatcher.service
$ test ! -e /etc/networkd-dispatcher/routable.d/50-example && echo 'example hook removed'
example hook removed
Disabling the whole service is the bigger rollback, and only sensible if you own every hook it runs:
$ sudo systemctl disable --now networkd-dispatcher.service
That stops every networkd-dispatcher hook, packaged or local. Bring it back with sudo systemctl enable --now networkd-dispatcher.service once you have checked the configuration.
Done means
- Version checked. You confirmed the installed networkd-dispatcher version and its help output.
- Right directory. The hook sits in the state directory matching the event you actually want to catch.
- Correctly owned. The hook is executable, root-owned, and named for predictable ordering.
- Service verified. The service is active and the journal shows your hook's message once a matching event fires.
- Startup risk understood. You know startup triggers can fire a hook the moment the service starts.
- Rollback ready. You can remove the example hook without disabling anyone else's network actions.