docker-net-dhcp

module
v2.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 31, 2026 License: GPL-3.0

README

docker-net-dhcp

Test Integration Dependencies Release OpenSSF Scorecard OpenSSF Best Practices Docs Sponsor

A Docker network plugin that leases every container's address from the DHCP server your LAN already runs, over bridge, macvlan or ipvlan, for IPv4 and IPv6. One docker network create, then networks: [lan-dhcp] in any Compose file. No static addresses, no sidecars, no plumbing per container.

The problem it solves: with plain macvlan or static addressing, a container can come back from an update or a restart with a different address, or two containers can end up on one address. This plugin leases each address from the LAN's DHCP server and, in bridge and macvlan mode, keeps one identity per container across restarts and upgrades, so the router's lease table and its reservations stay true.

Every release is run in a separate lab against real DHCP servers, Kea (single, hot-standby pair and behind a relay), ISC dhcpd (single and failover pair), dnsmasq, udhcpd, Pi-hole and OpenWrt, with Docker hosts on Debian 13, Ubuntu 24.04 and Debian 11 with Docker 20.10; a MikroTik RouterOS CHR cell is set up but has written no verdicts yet (latest lab results, how the lab works).

This is the successor of devplayer0/docker-net-dhcp, not a patched copy: 2.x has its own DHCP engine and its own lifecycle code.

sudo mkdir -p /var/lib/net-dhcp                      # once per host
docker plugin install ghcr.io/claymore666/docker-net-dhcp:v2.7.0   # -arm64 on arm64
docker network create -d ghcr.io/claymore666/docker-net-dhcp:v2.7.0 \
  --ipam-driver null -o mode=macvlan -o parent=eth0 lan-dhcp
docker run --rm -ti --network lan-dhcp alpine ip address show

The parts and what passes between them are drawn in docs/architecture.md.

It is a privileged plugin: it runs with host networking, the Docker socket and CAP_NET_ADMIN. The full list is under Requirements, and what each item is for is in SECURITY.md.

Feature list

  • The address is the LAN's. The router's lease table, its MAC reservations and, with -o register_dns=true, its DNS see the container as one more host.
  • IPv6 in the same shape as IPv4. -o ipv6_mode= picks DHCPv6, SLAAC, or whatever the router advertisement says, per network, with the same identity rules and the same counters. -o ipv6_pd=64 also asks for a delegated prefix, for a container that routes one. A network created with --ipv4=false (Docker 28 or later) runs no DHCPv4, and its containers get IPv6 only (#1135).
  • One identity per container, kept across restarts. In bridge and macvlan the plugin keeps the MAC, and with it the DHCP client id and the DHCPv6 DUID, across docker restart, a daemon restart and a plugin upgrade, and asks for the old address again. A MAC reservation on the router keeps matching.
  • ipvlan gets its own client id. ipvlan containers share the parent's MAC, so each one gets its own DHCP client id (option 61) and its own DUID (#895). That identity is new on every restart, so by default an ipvlan container does not keep its address across docker restart. The client_id driver option gives a container its own client id that keeps the address across a restart and a recreate (#219).
  • VLANs, sub-modes, MTU. -o vlan= puts a macvlan or ipvlan network on a tagged VLAN off the parent (#902); -o macvlan_mode= and -o ipvlan_mode= pick the kernel mode of each link (#905); -o mtu= sets the link MTU (#1037).
  • Bridge mode from a spare NIC. With -o parent= the plugin makes the bridge, enslaves the NIC and removes both with the network (#903); -o force_create=true overrides the firewall check on that bridge.
  • APIPA fallback while the DHCP server is down. With -o link_local_fallback=true a container starts on a 169.254 address (IPv4 only, bridge and macvlan), the plugin keeps asking and moves the container to the lease in place when one arrives (v2.3.0, #904, reference).
  • Monitoring. /Plugin.Health on the plugin socket reports the lease state and the counters; /metrics serves the same counters in Prometheus text format, always on the socket and on a TCP listener when METRICS_ADDR is set; -o audit_log=true appends every lease-lifecycle event to a ledger under the state directory (audit ledger).
  • Leases follow Docker's lifecycle. Renewal, rebind, NAK and expiry run in the plugin, one client per endpoint. -o release_lease= hands the address back at stop or at remove. Every Docker hook has a defined outcome, including a container that leaves before its lease binds, a network removed together with its bridge, and an endpoint Docker forgot.
  • Measured on Docker Engine 20.10 through 29. The engine matrix builds the plugin, creates networks in every documented shape, confirms a lease in the DHCP server's own log and keeps an address across docker restart, on every engine line, weekly and on every change to the measurement. Below 20.10 the plugin refuses to start and names the minimum.

Two shapes

You want Use
The container takes its address from the LAN, Docker keeps no pool --ipam-driver null (the default shape, all three modes)
Docker knows the address: docker run --ip, Compose ipv4_address, docker inspect the plugin as IPAM driver as well (--ipam-driver <plugin>), bridge and macvlan, IPv4 and IPv6; --ip6 and ipv6_address are served when an IPv6 --subnet is typed and ipv6_mode is dhcp (#1132)
ipvlan --ipam-driver null
IPv4 IPv6 as IPAM driver
bridge yes yes yes
macvlan yes yes yes
ipvlan yes yes no, needs a Docker change (#949)

Unsupported

  • ipvlan with this plugin as IPAM driver. Docker assigns a MAC to the ipvlan interface, and ipvlan interfaces cannot take one. Needs a change in Docker, not planned (#949). ipvlan networks use --ipam-driver null.
  • Docker Engine 19.03. Unmeasured, not planned.
  • IPv6-Only Preferred (DHCPv4 option 108, RFC 8925) is never requested. A Docker endpoint on a network with an IPv4 pool must hold an IPv4 address, which makes it an IPv4-requiring host, and RFC 8925 section 3.2 forbids such a host from asking for the option. The DHCP library implements the option for consumers that are not (#1027).

Where the plugin refuses, the message names the reason.

Planned

v2.7.0 adds a client_id driver option for one container (#219), an EBUSY error that names the children the plugin holds on the parent (#1372) and a SLAAC address that leaves no kernel prefix route behind (#1412). v2.8.0 is planned: an IPAM-mode restart that keeps the address on a DHCP server that files leases by MAC (#1416) and a fix for renewal behind a DHCP relay (#1419) (milestone). The full list, with what this project will not do, is on the roadmap.

How to check any of this

  • The DHCP exchange runs inside the plugin on the project's own library, dhcp-golib. No external DHCP client, no client process per container.
  • Tests assert on the wire and on the server: packet captures and the DHCP server's lease log, not the plugin's own counters (how this plugin is tested).
  • The real-server lab installs each server the way its users install it, on its own VMs, one isolated cell per server; every scenario is judged against the server's own lease table, and the evidence of each run is published (the lab, docker-net-dhcp-lab).
  • Every pull request carries a public review verdict, and every release ships signatures, SLSA provenance and an SBOM (verifying releases).

Requirements

  • Docker Engine 20.10 or newer. 20.10 is the lowest version this plugin is measured on. The engine matrix drives the whole baseline on every engine line from 20.10 to the current release: the plugin created from a local build and enabled, docker network create in bridge, macvlan and ipvlan mode, a lease confirmed in the DHCP server's own log, and an endpoint that keeps its address across docker restart. Pulling the published plugin from a registry is not part of that measurement. It runs weekly and on every change to the measurement. Below 20.10 the plugin refuses to start, and the refusal names the minimum and the engine it saw; an engine older than 19.03 is named by the API version it reported. 19.03 is unsupported because it is unmeasured: on a cgroup v2 host it cannot start a container at all, so nothing there tests this plugin. Every change is also tested against the engine the integration suite runs on, 29.9.2 today, read from the engine drift check in that run's Prepare the lane step.
  • Plugin interface docker.networkdriver/1.0, which is what the plugin manifest declares. The plugin negotiates the Docker API version with the daemon. It publishes both numbers on /Plugin.Health as engine_version and api_version. The minimum above is a version of the engine, not of the API. The one refusal on the API version is below 1.40, the oldest the plugin's Docker client speaks, which only engines older than 19.03 report.
  • One directory, created once per host, before docker plugin install (the line is in the quick start below). Docker will not create a missing bind source, so without it the install fails at start-up and leaves the plugin installed but disabled, after which the identical command answers only plugin ... already exists and names nothing. Recovery: the reference.
  • Architecture. linux/amd64 on the bare tag, linux/arm64 on the -arm64 tag. A Docker plugin cannot be installed from a multi-architecture manifest list, so the tag is how the architecture is chosen, in every snippet that names the image and not only the install line. Why, in full: Install, upgrade, uninstall.
  • Privileges. The manifest asks for host networking, the host PID namespace, the Docker socket, a bind mount of the state directory, a read-only bind mount of /var/run/docker, and CAP_NET_ADMIN, CAP_NET_RAW, CAP_SYS_ADMIN, CAP_SYS_PTRACE. docker plugin install prompts for the set; what each is for is in SECURITY.md.
  • Kernel link types. Bridge mode needs veth and bridge; macvlan and ipvlan each need the kernel module of the same name. A stock distribution kernel loads one the first time that link type is asked for, so there is normally nothing to do: measured on Linux 6.12, ipvlan was absent from lsmod before the first ip link add ... type ipvlan and present after, with no modprobe. A kernel built without the type, or a host where module loading is turned off, fails docker network create for that mode.
  • Root on the host. The plugin runs as root and its socket lives under /run/docker/plugins, which only root can read. Reading /Plugin.Health therefore needs sudo. Without it curl -s prints nothing and exits 7, which is what an absent socket also gives, so a permission problem looks like a stopped plugin.
  • Mode constraints. bridge expects a host bridge you maintain, or makes one from a spare NIC that stays up with no address (v2.3.0); macvlan and ipvlan attach to a host NIC and change nothing on it, at the cost of the kernel rule that a child cannot reach its own host's address. With -o vlan=<id> (v2.3.0) they attach to a VLAN sub-interface of that NIC, which the plugin creates when it is missing and removes with the last network on it. Both in macvlan / ipvlan modes.

Quick start

# Once per host, before the install. See Requirements above.
sudo mkdir -p /var/lib/net-dhcp

# amd64
docker plugin install ghcr.io/claymore666/docker-net-dhcp:v2.7.0
# arm64
docker plugin install ghcr.io/claymore666/docker-net-dhcp:v2.7.0-arm64

One network, created once. macvlan needs only a host NIC; bridge wants a bridge you bring yourself (bridge mode):

docker network create -d ghcr.io/claymore666/docker-net-dhcp:v2.7.0 \
  --ipam-driver null -o mode=macvlan -o parent=eth0 lan-dhcp

docker run --rm -ti --network lan-dhcp alpine ip address show

--ipam-driver null stops Docker handing out addresses that would collide with the real LAN. From v2.1.0 there is a second supported shape: name the plugin again in place of null and the leased address goes into Docker's own address management, which makes --ip and Compose's ipv4_address work.

docker network create -d ghcr.io/claymore666/docker-net-dhcp:v2.7.0 \
  --ipam-driver ghcr.io/claymore666/docker-net-dhcp:v2.7.0 \
  -o mode=macvlan -o parent=eth0 lan-dhcp

One of the two is required. On arm64 the -arm64 tag goes in these lines too, because a network records the tagged reference as its driver. Add -o ipv6_mode=dhcp for a DHCPv6 lease beside the v4 one, or -o ipv6_mode=slaac to take the address from the router's advertisement; auto reads the advertisement and does what it says, DHCPv6 where it asks for DHCPv6 and the prefix where it does not. -o ipv6=true is the short spelling of dhcp. They work on both lines. On the IPAM line --ipv6 needs a typed IPv6 --subnet <prefix> (a /126 or shorter) and -o ipv6_mode=dhcp: the plugin allocates no IPv6 pool of its own, so the subnet is the pool Docker keeps its books in (#1132). The DHCPv6 server picks each address, and --ip6 asks it for one; a reply naming another address than --ip6, or one outside the subnet, is refused and released, and the container does not start. Without a subnet --ipv6 is refused (#960), and -o ipv6=true or -o ipv6_mode=<mode> switches IPv6 on instead. The modes are set out in the driver reference, and the two shapes in the same page.

After that, plain Compose. No static addresses, no sidecar, nothing per container:

services:
  app:
    image: nginx
    networks: [lan-dhcp]

networks:
  lan-dhcp:
    external: true

Documentation

Published at https://claymore666.github.io/docker-net-dhcp/, one version per release; the same pages live in docs/.

Images go to GHCR (ghcr.io/claymore666/docker-net-dhcp:vX.Y.Z, primary) and are mirrored to Docker Hub under two names, claymore666/net-dhcp:vX.Y.Z and claymore666/docker-net-dhcp:vX.Y.Z. The two Hub names are the same image at the same digest; install from either. What a major, minor or patch release may change is in Versioning.

Origin and licence

This project is the successor of devplayer0/docker-net-dhcp (quiet since 2021); since 2.0 it runs its own DHCP engine.

GPL-3.0. See LICENSE.md. The upstream project is GPL-3.0 and this derivative stays under the same licence.

Verifying releases

Every release from v1.1.0 is cosign-signed (keyless) on both registries and ships an SBOM; SLSA build provenance is attested for the GHCR image only, so verify provenance against the ghcr.io reference. Both commands need cosign v3 or newer. v2 cannot read the bundle format the release signs with, and it fails in a way that looks like a broken signature. Replace VERSION:

cosign verify ghcr.io/claymore666/docker-net-dhcp:VERSION \
  --certificate-identity-regexp '^https://github.com/claymore666/docker-net-dhcp/.github/workflows/release.yml@' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

gh attestation verify oci://ghcr.io/claymore666/docker-net-dhcp:VERSION --repo claymore666/docker-net-dhcp

The whole procedure, including rebuilding the binaries yourself, is in Verifying releases.

Project & community

  • Security policy / vulnerability reporting: SECURITY.md. Do not open a public issue for a vulnerability.
  • Bug reports & feature requests: the issue forms. Include the plugin version, your Docker version, the mode, and the plugin log. Docker has no plugin logs subcommand.
  • Questions: ask in Discussions; each issue form stamps a type label, so a question has no honest home on the tracker.
  • Governance & code of conduct: GOVERNANCE.md, CODE_OF_CONDUCT.md.
  • Support: if the project saves you time, you can chip in via PayPal.

Contributing

Contributions are welcome. Open a pull request against the dev branch; Contributing is the whole of what is asked, and make check runs the fast CI lane locally in about a minute. In short:

  • Go code is gofmt-formatted and passes go vet and staticcheck; shell and workflow files pass shellcheck and actionlint.
  • New functionality is expected to ship with tests, and a per-package coverage ratchet enforces that at release time.
  • Commits and pull request descriptions carry no AI-assistant attribution and the commit author is a person. The attribution check reads every commit and the description.
  • Every required check must be green; branch protection holds the list.
  • Looking for somewhere to start? There are no starter tasks open at the moment. The ones that were seeded were closed as the work they described landed, and this section says so instead of sending you to an empty list. If you would like a first task, ask in Discussions and say what interests you: a subsystem, a bug you hit, a piece of the documentation you found thin. One will be scoped against it. When starter tasks exist again they carry the good first issue label and this section links to them.

This is solo-maintained, so please allow a few days for a response.

Directories

Path Synopsis
cmd
net-dhcp command
pkg
buildinfo
Package buildinfo carries what this binary was built from.
Package buildinfo carries what this binary was built from.
dhcp
Package dhcp is the chassis between the plugin and the in-process DHCP library, holding the Docker side only (#899).
Package dhcp is the chassis between the plugin and the in-process DHCP library, holding the Docker side only (#899).
plugin
Package plugin is the Docker side of docker-net-dhcp: it serves the two libnetwork contracts that make this a network plugin, /NetworkDriver.* and /IpamDriver.*, on one Unix socket in one process, beside two paths that belong to neither contract and are served on the same socket anyway: /Plugin.Health and /metrics.
Package plugin is the Docker side of docker-net-dhcp: it serves the two libnetwork contracts that make this a network plugin, /NetworkDriver.* and /IpamDriver.*, on one Unix socket in one process, beside two paths that belong to neither contract and are served on the same socket anyway: /Plugin.Health and /metrics.
test
arm64-netboot/nfs-watchdog command
nfs-watchdog pets the SoC hardware watchdog only while the root filesystem still answers, so an arm64 CI host whose NFS server disappears resets itself instead of wedging (#632).
nfs-watchdog pets the SoC hardware watchdog only while the root filesystem still answers, so an arm64 CI host whose NFS server disappears resets itself instead of wedging (#632).
integration/harness
No integration tag: a pure parser driven against verbatim output from the suite's container image.
No integration tag: a pure parser driven against verbatim output from the suite's container image.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL