Unbound: Conditionally Include Only Existing (Docker) Interfaces
This article presents a simple and reliable solution to flexibly configure Unbound with interfaces that may not exist when Unbound starts. The setup described below is based on my home server Unbound installation but should work well in other scenarios, too.
Problem: Unbound Doesn’t Start if Interfaces Don’t Exist
I need Unbound to bind to and listen on the Caddy Docker interface. That interface is created at boot time when the Docker service is started. Unbound, however, is started before Docker. At that point in time, however, the Docker interface doesn’t exist yet, and Unbound obstinately refuses do start if one its configured interface doesn’t exist.
Solution: Include Only Existing Interfaces
After quite a bit of deliberation I came up with the following solution.
We’re adding an include file to Unbound’s configuration. The content of the include file is generated dynamically by a pre start script of Unbound’s systemd service. Said script checks for the Docker interface’s existence and only adds it to the include file if it is found.
Additionally, we’re adding a post start command to Docker’s systemd service. This command restarts Unbound to ensure that Unbound re-checks for the Docker interface’s existence once Docker is up.
Preparation
Pre Start Script
Create the file /usr/local/bin/unbound-conditional-include.sh
with the following content:
#!/bin/bash
# The Unbound config include file this script creates
INCLUDE_FILE=/etc/unbound/unbound.conf.d/conditional-include.conf
# Interfaces to test for existence
declare -a interfaces2test=("docker_caddy" "dummy_nx_if_2")
# Create/empty the include file
echo "# Conditional include file for Unbound." > ${INCLUDE_FILE}
echo "# Automatically generated. Do not modify.\\n" >> ${INCLUDE_FILE}
# Loop through and test all interfaces defined above
for i in "${interfaces2test[@]}"
do
ip a show $i up 1>/dev/null 2>&1
if [ $? -eq 0 ]; then
# Interface is up
echo " interface: $i" >> ${INCLUDE_FILE}
fi
done
Make unbound-conditional-include.sh
executable:
chmod a+x /usr/local/bin/unbound-conditional-include.sh
Unbound Service Override
Create a unit file snippet for the Unbound service (under the hood, this creates the override file /etc/systemd/system/unbound.service.d/override.conf
):
systemctl edit unbound
Paste the following two lines in the editor window that opened and save the file:
[Service]
ExecStartPre=+/usr/local/bin/unbound-conditional-include.sh
Note the plus (+) sign before the command. It instructs systemd to run the command with full root privileges (docs). Without this we couldn’t write to /etc/unbound
Test
Restart Unbound and make sure that /etc/unbound/unbound.conf.d/conditional-include.conf
is created:
systemctl restart unbound
la /etc/unbound/unbound.conf.d/
Unbound Configuration
Edit your Unbound configuration file /etc/unbound/unbound.conf
, adding an include
directive after the interface
definitions:
# Include additional interfaces from a config file that is dynamically generated to only contain existing interfaces.
include: /etc/unbound/unbound.conf.d/conditional-include.conf
Check and Restart
Check the new Unbound configuration and restart the daemon to make sure everything works:
unbound-checkconf
systemctl restart unbound
Re-Evaluation When Docker Is Up
Once Docker is started, Unbound should bind to Docker’s interface. To achieve that, we add a command to its service configuration that restarts Unbound.
systemctl edit docker
Paste the following two lines in the editor window that opened and save the file:
[Service]
ExecStartPost=+systemctl restart unbound
Notes
Docker Interfaces When Docker is Stopped
When the Docker service/daemon is stopped, its interfaces do not go away. This means that there is not need to restart Unbound in order to trigger a re-run of the script that generates the configuration include.