Hello Simon,

on my first tries to start multiple dnsmasq instances on Debian 9 "Stretch" with systemd I faced several issues and created Debian bug report #914305 [1]. Yesterday I finally managed to spend several hours on the issue and found a clean solution for it. While preparing the text for the bug report I recognized that you're the maintainer of the Debian packages, so I decided to write to the dnsmasq mailing list first.

[1] https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=914305



systemd unit files [2] allow to be used for multiple instances when the service unit file name ends with the at symbol (@). Then the service can be enabled with an instance name following the at symbol, e.g. `systemctl enable dnsmasq@main.service`. The instance name is available in an escaped format in variable %i (lower case) when the unit file is processed. The attached unit file dnsmasq@.service passes the escaped instance name to the init.d script (minor changes to the code plus `mv -v /lib/systemd/system/dnsmasq.service /lib/systemd/system/dnsmasq@.service`).

The 2nd attached file is the updated init.d script for dnsmasq.
It now recognizes the instance name via the second script paramater and uses it wherever needed or possible (default file, pid file, resolvconf protocol, log entries).

Additionally three special cases had to be handled when running multiple instances of dnsmasq: a) The original systemd unit file wants to check the configuration before starting the service but does not honor the settings from the default file (conf file and dir).
   Therefore the option checkconfig was added to the init.d script.
I don't know if there's a common SysInit V standard name for such a function [3]. b) `mkdir /run/dnsmasq` in the init.d script can fail as unit files are run in parallel, so the directory has to be checked again if mkdir failed. c) Only one dnsmasq instance should be the dns resolver for the local system and should bind to localhost. Therefore revived DNSMASQ_EXCEPT="lo" in the default file (3rd attached file).

Additional changes to the files are typo corrections.

[2] https://www.freedesktop.org/software/systemd/man/systemd.unit.html
[3] https://www.debian.org/doc/debian-policy/ch-opersys.html#writing-the-scripts



For testing I installed openresolv and dnsmasq on latest Debian 9 "Stretch" and created some virtual network interfaces via systemd [4]. The main dnsmasq instance shall run on the real NIC while special instances shall run on the extra virtual NICs (dnsextra*).

Stopped and disabled the original service from the Debian dnsmasq package:
`systemctl stop dnsmasq.service`
`systemctl disable dnsmasq.service`
`systemctl status dnsmasq.service`

Prepared dnsmasq systemd unit file for instances by renaming and updating it: `mv -v /lib/systemd/system/dnsmasq.service /lib/systemd/system/dnsmasq@.service`

As instance enabled systemd unit files have to be used with an instance name I decided to name the default dnsmasq instance simply "main". Not to break SysInit V compatibility a symbol link was used for the "renaming" of the default file.
`ln -s -T dnsmasq /etc/default/dnsmasq.main`
(P.S. Other idea would be to default INSTANCE in init.d to 'main' when instance name not given.)

Updated also init.d script and normal default file.

Then prepared two dnsmasq instances:
1. Default file for main instance (/etc/default/dnsmasq.main)
Changed to DNSMASQ_OPTS="--bind-dynamic --except-interface=dnsextra*"
This way it will avoid binding to the extra virtual NICs while still recognizing new addresses and other new NICs, and it will also be the DNS resolver for the local system.

2. New default file for first extra instance (/etc/default/dnsmasq.extra01) Copied from default file of main instance via `cp /etc/default/dnsmasq /etc/default/dnsmasq.extra01`
Changed the following settings for this extra instance:
* DNSMASQ_OPTS="--bind-dynamic --interface=dnsextra01 --address=/heise.de/192.168.0.250"
  Binds to one explicit interface while still recognizing new addresses.
  One modified address resolution for testing with dig.
* IGNORE_RESOLVCONF=yes
Always using /etc/resolv.conf therefore either using the dnsmasq main instance (if it is started) as upstream dns server or the upstream server from resolvconf (e.g. via DHCP).
* DNSMASQ_EXCEPT="lo"
Avoid binding to localhost and also not being used as the DNS resolver for the local system.

With this setup I could start/stop any dnsmasq instance while keeping a working DNS setup. Additionally I could assign an explicit dnsmasq instance to any dhcp client.

[4] https://gist.github.com/maddes-b/e487d1f95f73f5d40805315f0232d5d9



I hope that I explained everything understandably, completely and in correct English. Any feedback is welcome and it would be great to see this in Debian 10 :)

Kind regards
Maddes

[Unit]
Description=dnsmasq (%i) - A lightweight DHCP and caching DNS server
Requires=network.target
Wants=nss-lookup.target
Before=nss-lookup.target
After=network.target

[Service]
Type=forking
PIDFile=/run/dnsmasq/dnsmasq.%i.pid

# Test the config file and refuse starting if it is not valid.
ExecStartPre=/etc/init.d/dnsmasq checkconfig "%i"

# We run dnsmasq via the /etc/init.d/dnsmasq script which acts as a
# wrapper picking up extra configuration files and then execs dnsmasq
# itself, when called with the "systemd-exec" function.
ExecStart=/etc/init.d/dnsmasq systemd-exec "%i"

# The systemd-*-resolvconf functions configure (and deconfigure)
# resolvconf to work with the dnsmasq DNS server. They're called like
# this to get correct error handling (ie don't start-resolvconf if the
# dnsmasq daemon fails to start).
ExecStartPost=/etc/init.d/dnsmasq systemd-start-resolvconf "%i"
ExecStop=/etc/init.d/dnsmasq systemd-stop-resolvconf "%i"


ExecReload=/bin/kill -HUP $MAINPID

[Install]
WantedBy=multi-user.target
#!/bin/sh
### BEGIN INIT INFO
# Provides:       dnsmasq
# Required-Start: $network $remote_fs $syslog
# Required-Stop:  $network $remote_fs $syslog
# Default-Start:  2 3 4 5
# Default-Stop:   0 1 6
# Description:    DHCP and DNS server
### END INIT INFO

# Don't exit on error status
set +e

PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DAEMON=/usr/sbin/dnsmasq
NAME=dnsmasq
DESC="DNS forwarder and DHCP server"
INSTANCE="${2}"

# Most configuration options in /etc/default/dnsmasq are deprecated
# but still honoured.
ENABLED=1
if [ -r /etc/default/${NAME}${INSTANCE:+.${INSTANCE}} ]; then
    . /etc/default/${NAME}${INSTANCE:+.${INSTANCE}}
fi

# Get the system locale, so that messages are in the correct language, and the
# charset for IDN is correct
if [ -r /etc/default/locale ]; then
    . /etc/default/locale
    export LANG
fi

# The following test ensures the dnsmasq service is not started, when the
# package 'dnsmasq' is removed but not purged, even if the dnsmasq-base
# package is still in place.
test -e /usr/share/dnsmasq/installed-marker || exit 0

test -x ${DAEMON} || exit 0

# Provide skeleton LSB log functions for backports which don't have LSB functions.
if [ -f /lib/lsb/init-functions ]; then
    . /lib/lsb/init-functions
else
    log_warning_msg () {
        echo "${@}."
    }

    log_success_msg () {
        echo "${@}."
    }

    log_daemon_msg () {
        echo -n "${1}: ${2}"
    }

    log_end_msg () {
        if [ "${1}" -eq 0 ]; then
            echo "."
        elif [ "${1}" -eq 255 ]; then
            /bin/echo -e " (warning)."
        else
            /bin/echo -e " failed!"
        fi
    }
fi

# RESOLV_CONF:
# If the resolvconf package is installed then use the resolv conf file
# that it provides as the default.  Otherwise use /etc/resolv.conf as
# the default.
#
# If IGNORE_RESOLVCONF is set in /etc/default/dnsmasq or an explicit
# filename is set there then this inhibits the use of the resolvconf-provided
# information.
#
# Note that if the resolvconf package is installed it is not possible to
# override it just by configuration in /etc/dnsmasq.conf, it is necessary
# to set IGNORE_RESOLVCONF=yes in /etc/default/dnsmasq.

if [ ! "${RESOLV_CONF}" ] &&
   [ "${IGNORE_RESOLVCONF}" != "yes" ] &&
   [ -x /sbin/resolvconf ]
then
    RESOLV_CONF=/run/dnsmasq/resolv.conf
fi

for INTERFACE in ${DNSMASQ_INTERFACE}; do
    DNSMASQ_INTERFACES="${DNSMASQ_INTERFACES} -i ${INTERFACE}"
done

for INTERFACE in ${DNSMASQ_EXCEPT}; do
    DNSMASQ_INTERFACES="${DNSMASQ_INTERFACES} -I ${INTERFACE}"
done

if [ ! "${DNSMASQ_USER}" ]; then
   DNSMASQ_USER="dnsmasq"
fi

# This tells dnsmasq to ignore DNS requests that don't come from a local network.
# It's automatically ignored if  --interface --except-interface, --listen-address
# or --auth-server exist in the configuration, so for most installations, it will
# have no effect, but for otherwise-unconfigured installations, it stops dnsmasq
# from being vulnerable to DNS-reflection attacks.

DNSMASQ_OPTS="${DNSMASQ_OPTS} --local-service"

# If the dns-root-data package is installed, then the trust anchors will be
# available in ROOT_DS, in BIND zone-file format. Reformat as dnsmasq
# --trust-anchor options.

ROOT_DS="/usr/share/dns/root.ds"

if [ -f ${ROOT_DS} ]; then
   DNSMASQ_OPTS="${DNSMASQ_OPTS} `sed -e s/". IN DS "/--trust-anchor=.,/ -e s/" "/,/g ${ROOT_DS} | tr '\n' ' '`"
fi

start()
{
    # Return
    #   0 if daemon has been started
    #   1 if daemon was already running
    #   2 if daemon could not be started

    # /run may be volatile, so we need to ensure that
    # /run/dnsmasq exists here as well as in postinst
    if [ ! -d /run/dnsmasq ]; then
        mkdir /run/dnsmasq || { [ -d /run/dnsmasq ] || return 2 ; }
        chown dnsmasq:nogroup /run/dnsmasq || return 2
    fi

    start-stop-daemon --start --quiet --pidfile /run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid --exec ${DAEMON} --test > /dev/null || return 1
    start-stop-daemon --start --quiet --pidfile /run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid --exec ${DAEMON} -- \
        -x /run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid \
        ${MAILHOSTNAME:+ -m ${MAILHOSTNAME}} \
        ${MAILTARGET:+ -t ${MAILTARGET}} \
        ${DNSMASQ_USER:+ -u ${DNSMASQ_USER}} \
        ${DNSMASQ_INTERFACES:+ ${DNSMASQ_INTERFACES}} \
        ${DHCP_LEASE:+ -l ${DHCP_LEASE}} \
        ${DOMAIN_SUFFIX:+ -s ${DOMAIN_SUFFIX}} \
        ${RESOLV_CONF:+ -r ${RESOLV_CONF}} \
        ${CACHESIZE:+ -c ${CACHESIZE}} \
        ${CONFIG_DIR:+ -7 ${CONFIG_DIR}} \
        ${DNSMASQ_OPTS:+ ${DNSMASQ_OPTS}} \
        || return 2
}

start_resolvconf()
{
# If interface "lo" is explicitly disabled in /etc/default/dnsmasq
# Then dnsmasq won't be providing local DNS, so don't add it to
# the resolvconf server set.
    for interface in ${DNSMASQ_EXCEPT}; do
        [ ${interface} = lo ] && return
    done

    if [ -x /sbin/resolvconf ] ; then
        echo "nameserver 127.0.0.1" | /sbin/resolvconf -a lo.${NAME}${INSTANCE:+.${INSTANCE}}
    fi
    return 0
}

stop()
{
    # Return
    #   0 if daemon has been stopped
    #   1 if daemon was already stopped
    #   2 if daemon could not be stopped
    #   other if a failure occurred
    start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile /run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid --name ${NAME}
}

stop_resolvconf()
{
    if [ -x /sbin/resolvconf ] ; then
        /sbin/resolvconf -d lo.${NAME}${INSTANCE:+.${INSTANCE}}
    fi
    return 0
}

status()
{
    # Return
    #   0 if daemon is running
    #   1 if daemon is dead and pid file exists
    #   3 if daemon is not running
    #   4 if daemon status is unknown
    start-stop-daemon --start --quiet --pidfile /run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid --exec ${DAEMON} --test > /dev/null
    case "${?}" in
      0) [ -e "/run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid" ] && return 1 ; return 3 ;;
      1) return 0 ;;
      *) return 4 ;;
    esac
}

case "${1}" in
  start)
    test "${ENABLED}" != "0" || exit 0
    log_daemon_msg "Starting ${DESC}" "${NAME}${INSTANCE:+.${INSTANCE}}"
    start
    case "${?}" in
      0)
        log_end_msg 0
        start_resolvconf
        exit 0
        ;;
      1)
        log_success_msg "(already running)"
        exit 0
        ;;
      *)
        log_end_msg 1
        exit 1
        ;;
    esac
    ;;
  stop)
    stop_resolvconf
    if [ "${ENABLED}" != "0" ]; then
        log_daemon_msg "Stopping ${DESC}" "${NAME}${INSTANCE:+.${INSTANCE}}"
    fi
    stop
    RETVAL="${?}"
    if [ "${ENABLED}" = "0" ]; then
        case "${RETVAL}" in
          0) log_daemon_msg "Stopping ${DESC}" "${NAME}${INSTANCE:+.${INSTANCE}}"; log_end_msg 0 ;;
        esac
        exit 0
    fi
    case "${RETVAL}" in
      0) log_end_msg 0 ; exit 0 ;;
      1) log_warning_msg "(not running)" ; exit 0 ;;
      *) log_end_msg 1; exit 1 ;;
    esac
    ;;
  checkconfig)
    ${DAEMON} --test ${CONFIG_DIR:+ -7 ${CONFIG_DIR}} ${DNSMASQ_OPTS:+ ${DNSMASQ_OPTS}} >/dev/null 2>&1
    RETVAL="${?}"
    exit ${RETVAL}
    ;;
  restart|force-reload)
    test "${ENABLED}" != "0" || exit 1
    ${DAEMON} --test ${CONFIG_DIR:+ -7 ${CONFIG_DIR}} ${DNSMASQ_OPTS:+ ${DNSMASQ_OPTS}} >/dev/null 2>&1
    if [ ${?} -ne 0 ]; then
        NAME="configuration syntax check"
        RETVAL="2"
    else
        stop_resolvconf
        stop
        RETVAL="${?}"
    fi
    log_daemon_msg "Restarting ${DESC}" "${NAME}${INSTANCE:+.${INSTANCE}}"
    case "${RETVAL}" in
      0|1)
        sleep 2
        start
        case "${?}" in
          0)
            log_end_msg 0
            start_resolvconf
            exit 0
            ;;
          *)
            log_end_msg 1
            exit 1
            ;;
        esac
        ;;
      *)
        log_end_msg 1
        exit 1
        ;;
    esac
    ;;
  status)
    log_daemon_msg "Checking ${DESC}" "${NAME}${INSTANCE:+.${INSTANCE}}"
    status
    case "${?}" in
      0) log_success_msg "(running)" ; exit 0 ;;
      1) log_success_msg "(dead, pid file exists)" ; exit 1 ;;
      3) log_success_msg "(not running)" ; exit 3 ;;
      *) log_success_msg "(unknown)" ; exit 4 ;;
    esac
    ;;
  dump-stats)
    kill -s USR1 `cat /run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid`
    ;;
  systemd-start-resolvconf)
    start_resolvconf
    ;;
  systemd-stop-resolvconf)
    stop_resolvconf
    ;;
  systemd-exec)
    # /run may be volatile, so we need to ensure that
    # /run/dnsmasq exists here as well as in postinst
    if [ ! -d /run/dnsmasq ]; then
        mkdir /run/dnsmasq || { [ -d /run/dnsmasq ] || return 2 ; }
        chown dnsmasq:nogroup /run/dnsmasq || return 2
    fi
    exec ${DAEMON} -x /run/dnsmasq/${NAME}${INSTANCE:+.${INSTANCE}}.pid \
        ${MAILHOSTNAME:+ -m ${MAILHOSTNAME}} \
        ${MAILTARGET:+ -t ${MAILTARGET}} \
        ${DNSMASQ_USER:+ -u ${DNSMASQ_USER}} \
        ${DNSMASQ_INTERFACES:+ ${DNSMASQ_INTERFACES}} \
        ${DHCP_LEASE:+ -l ${DHCP_LEASE}} \
        ${DOMAIN_SUFFIX:+ -s ${DOMAIN_SUFFIX}} \
        ${RESOLV_CONF:+ -r ${RESOLV_CONF}} \
        ${CACHESIZE:+ -c ${CACHESIZE}} \
        ${CONFIG_DIR:+ -7 ${CONFIG_DIR}} \
        ${DNSMASQ_OPTS:+ ${DNSMASQ_OPTS}}
    ;;
  *)
    echo "Usage: /etc/init.d/${NAME} {start|stop|restart|force-reload|dump-stats|status}" >&2
    exit 3
    ;;
esac

exit 0
# This file has six functions:
# 1) to completely disable starting this dnsmasq instance
# 2) to set DOMAIN_SUFFIX by running `dnsdomainname`
# 3) to select an alternative config file
#    by setting DNSMASQ_OPTS to --conf-file=<file>
# 4) to tell dnsmasq to read the files in /etc/dnsmasq.d for
#    more configuration variables.
# 5) to stop the resolvconf package from controlling dnsmasq's
#    idea of which upstream nameservers to use.
# 6) to avoid using this dnsmasq instance as the system's default resolver
#    by setting DNSMASQ_EXCEPT="lo"
# For upgraders from very old versions, all the shell variables set
# here in previous versions are still honored by the init script
# so if you just keep your old version of this file nothing will break.

#DOMAIN_SUFFIX=`dnsdomainname`
#DNSMASQ_OPTS="--conf-file=/etc/dnsmasq.alt"

# Whether or not to run the dnsmasq daemon; set to 0 to disable.
ENABLED=1

# By default search this drop directory for configuration options.
# Libvirt leaves a file here to make the system dnsmasq play nice.
# Comment out this line if you don't want this. The dpkg-* are file
# endings which cause dnsmasq to skip that file. This avoids pulling
# in backups made by dpkg.
CONFIG_DIR=/etc/dnsmasq.d,.dpkg-dist,.dpkg-old,.dpkg-new

# If the resolvconf package is installed, dnsmasq will use its output
# rather than the contents of /etc/resolv.conf to find upstream
# nameservers. Uncommenting this line inhibits this behaviour.
# Note that including a "resolv-file=<filename>" line in
# /etc/dnsmasq.conf is not enough to override resolvconf if it is
# installed: the line below must be uncommented.
#IGNORE_RESOLVCONF=yes

# If the resolvconf package is installed, dnsmasq will tell resolvconf
# to use dnsmasq under 127.0.0.1 as the system's default resolver.
# Uncommenting this line inhibits this behaviour.
#DNSMASQ_EXCEPT="lo"
_______________________________________________
Dnsmasq-discuss mailing list
Dnsmasq-discuss@lists.thekelleys.org.uk
http://lists.thekelleys.org.uk/mailman/listinfo/dnsmasq-discuss

Reply via email to