Sign inSign up

modem7/dnsmasq-relay

By modem7

•Updated about 2 months ago

DHCP relay across VLANs using dnsmasq

Image
0

310

modem7/dnsmasq-relay repository overview

⁠dnsmasq-relay

Docker Pulls Docker Image Size (tag) status-badge Lint Dockerfile GitHub latest commit

"Buy Me A Coffee"

DHCP relay across VLANs and subnets, for routers or switches that don't have a built-in relay function (unlike, for example, Ubiquiti/UniFi gear, which does).

⁠What this does

This image relays DHCPv4 and DHCPv6 broadcast/multicast traffic between VLANs using dnsmasq⁠'s --dhcp-relay mode. It replaces modem7/DHCP-Relay⁠, which wrapped ISC's dhcrelay - upstream ISC-DHCP has had no releases since 2022 and is EOL.

The original plan was to move to Kea⁠, ISC's supported DHCP successor, but Kea turned out to have no relay agent of its own - it's server-only. dnsmasq's --dhcp-relay mode is actively maintained upstream and purpose-built for exactly this, so it's used here instead.

This image is relay-only: it never runs as a DNS server, DHCP server, or TFTP server, regardless of configuration.

⁠Prerequisites

dnsmasq's relay identifies which VLAN a request arrived on by an IP address already assigned to a host interface - not by interface name. Before configuring DHCP_RELAYS (below), each VLAN you want to relay from needs its own IP already set up on the host, for example via a tagged VLAN sub-interface.

netplan example (/etc/netplan/vlans.yaml):

network:
  version: 2
  vlans:
    vlan10:
      id: 10
      link: eth0
      addresses: [10.0.10.1/24]
    vlan20:
      id: 20
      link: eth0
      addresses: [10.0.20.1/24]

systemd-networkd example (/etc/systemd/network/10-vlan10.netdev + /etc/systemd/network/10-vlan10.network):

# 10-vlan10.netdev
[NetDev]
Name=vlan10
Kind=vlan

[VLAN]
Id=10
# 10-vlan10.network
[Match]
Name=vlan10

[Network]
Address=10.0.10.1/24

If you're relaying DHCPv6 without network_mode: host (see Quick start, below), read Docker's IPv6 documentation⁠ first - Docker's own virtual networking has IPv6 support disabled by default, separately from anything this image does. Host networking (the default in the compose example below) bypasses Docker's virtual networking entirely, sidestepping this.

⁠Configuration

VariableRequiredDescription
DHCP_RELAYSYes, unless passing raw dnsmasq flags as the container commandSemicolon-separated list of relay pairs: <local-ip>,<server-ip>[#port][,iface];.... local-ip must already be assigned to a host interface (see Prerequisites). iface, if given, is an anti-spoofing filter controlling which interface replies are accepted on - not a second relay-side interface.
DHCP_RELAY_EXTRA_ARGSNoSpace-separated raw dnsmasq flags, appended as-is. Escape hatch for anything DHCP_RELAYS' syntax doesn't cover.
DHCP_RELAY_VERBOSENoSet to 1 to add dnsmasq's --log-dhcp, logging every relayed DHCP transaction. Start here when troubleshooting "DHCP isn't crossing this VLAN."

Examples:

# Single VLAN relayed to one DHCP server
DHCP_RELAYS="10.0.10.1,10.0.0.5"

# Three VLANs, all relayed to the same server
DHCP_RELAYS="10.0.10.1,10.0.0.5;10.0.20.1,10.0.0.5;10.0.30.1,10.0.0.5"

# Mixed v4 and v6 in the same container
DHCP_RELAYS="10.0.10.1,10.0.0.5;fd00:10::1,fd00::5"

If the container is given an explicit command instead (e.g. via compose's command:), it's exec'd unchanged - DHCP_RELAYS and the other env vars are ignored in that case.

⁠Quick start

docker run -d \
  --network host \
  --cap-drop ALL --cap-add NET_BIND_SERVICE --cap-add NET_ADMIN --cap-add NET_RAW \
  -e DHCP_RELAYS="10.0.10.1,10.0.0.5;10.0.20.1,10.0.0.5" \
  modem7/dnsmasq-relay:latest

Or with compose (see docker-compose.yml⁠ in this repo):

services:
  dnsmasq-relay:
    image: modem7/dnsmasq-relay:latest
    container_name: dnsmasq-relay
    environment:
      DHCP_RELAYS: "10.0.10.1,10.0.0.5;10.0.20.1,10.0.0.5;10.0.30.1,10.0.0.5"
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
      - NET_ADMIN
      - NET_RAW
    network_mode: host
    restart: always
    mem_limit: 20m
    mem_reservation: 5m

⁠Security notes

The container runs as a non-root user (the dnsmasq Alpine package's own system user) with exactly three capabilities, verified empirically by starting the container with --cap-drop ALL and adding capabilities back one at a time until dnsmasq stopped reporting a missing one:

  • NET_BIND_SERVICE - DHCP relay listens on ports 67/547, which are privileged (<1024).
  • NET_ADMIN - required by dnsmasq's relay path specifically.
  • NET_RAW - needed to reply to clients that don't have an IP yet.

This is one more capability than the old dhcrelay-based image needed (NET_RAW + NET_BIND_SERVICE only) - NET_ADMIN isn't a broader grant than necessary, it's just what dnsmasq's relay implementation actually requires.

⁠Health checking & troubleshooting

The container's HEALTHCHECK is process-liveness based (confirms dnsmasq is still PID 1's process). If DHCP requests aren't reaching the far side of a relay, set DHCP_RELAY_VERBOSE=1 first - it logs every relayed DHCP transaction via docker logs, which is almost always enough to see where the conversation is breaking down. Also check the startup log line: the container prints a plain-English summary of what it parsed from DHCP_RELAYS (e.g. Relaying: 10.0.10.1 -> 10.0.0.5 (v4)) before starting dnsmasq, confirming the config was understood as intended.

⁠Image tags

Tags track dnsmasq's actual packaged version (e.g. 2.92), not a build counter, so you can tell at a glance what's inside.

⁠Licence

MIT⁠

Tag summary

Content type

Image

Digest

sha256:d551c3813…

Size

4.1 MB

Last updated

about 2 months ago

docker pull modem7/dnsmasq-relay