When machines on an intranet need IP addresses handed out automatically, or when a particular device should get the same address every time (bound by MAC), you need a DHCP server. This post uses Rocky Linux 9 to record the complete flow — install, configure, syntax check, start, and client-side verification. Every step was actually run inside a Rocky Linux 9 container, including making dhclient and dhcpd complete the four-step negotiation within the same container and confirming that the bound MAC hit exactly the fixed IP. A CentOS 7 differences table is attached at the end, ready to consult when migrating older environments.

One well-configured dhcpd.conf can cover both classes of need at once: the dynamic pool hands out addresses by range for ordinary devices, and host blocks hand designated addresses by MAC to devices that “must have a fixed IP”.

Prerequisites

  • OS: Rocky Linux 9 (the tested environment for this post is a rockylinux:9 container, dhcp-server 4.4.2-20, aarch64)
  • Privileges: root or sudo
  • Network: the server itself needs a static address in the target subnet (192.168.254.2/24 below), and there must be no other DHCP server in that subnet — when two DHCP servers answer at once, which address a client gets depends purely on who replies first; this is the classic root cause of the “IP works sometimes and not others” problem on intranets
  • Ports: DHCP uses UDP 67 (server) / 68 (client); the firewall must allow them

Install the DHCP Server

el9 split ISC DHCP into three packages: the server dhcp-server, the client dhcp-client, and the shared files dhcp-common. Install the server:

1
dnf install -y dhcp-server

After installation, confirm the version — it should print a package name starting with dhcp-server-4.4.2:

Figure 1

/usr/sbin/dhcpd is now in place, the main configuration lives at /etc/dhcp/dhcpd.conf, and an annotated example config is available at /usr/share/doc/dhcp-server/dhcpd.conf.example — copy away.

Configure the DHCP Server

Back up the original configuration file before touching it:

1
2
cp /etc/dhcp/dhcpd.conf /etc/dhcp/dhcpd.conf.backup
vi /etc/dhcp/dhcpd.conf

The configuration file has two parts: global lease parameters and the subnet configuration; the host block is the key to binding a fixed IP by MAC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Global configuration parameters
default-lease-time 600;
max-lease-time 7200;

# Subnet configuration
subnet 192.168.254.0 netmask 255.255.255.0 {
range 192.168.254.151 192.168.254.200; # Dynamic IP address range
option routers 192.168.254.2; # Router address
option subnet-mask 255.255.255.0; # Subnet mask

# Bind an IP address by MAC address
host myclient {
hardware ethernet 00:11:22:33:44:55; # The client's MAC address
fixed-address 192.168.254.150; # Fixed IP assigned to that MAC
}
}

Note that the range starts at 151, deliberately staying clear of the bound address 150 — this was not written casually. I specifically verified the counter-example in the container: with the range set to 150-200, a client whose MAC did not match the host declaration got 192.168.254.150 right off the bat, which is simply the first free address the dynamic pool handed out. When the actually-bound device then connected, the two machines would fight over the same IP. The bound address must be inside the subnet and outside the range — a lesson genuinely learned the hard way; details in the notes section at the end.

Run the Syntax Check Before Anything Else

dhcpd ships with a -t test mode — mandatory before starting:

1
dhcpd -t -cf /etc/dhcp/dhcpd.conf

An exit code of 0 means the syntax is correct. In testing you will see one WARNING — it doesn’t block startup, but it says something worth knowing:

Figure 2

The warning means: host declarations are global — they are not limited to whichever subnet block you write them in. The config above places host myclient inside the subnet, and intuition says “this binding only applies to the 192.168.254.0/24 subnet”. The actual behavior: as long as the requesting MAC is 00:11:22:33:44:55, dhcpd will try to hand it the fixed-address no matter which subnet it asks from. On a single-subnet small intranet this has no impact; but if you later add a second subnet, move all host blocks to the top level of the configuration, so that the semantics and the actual behavior agree.

Start and Enable the DHCP Service

el9’s dhcpd behaves completely differently from CentOS 7: it only listens on interfaces whose addresses fall inside declared subnets — manually specifying a NIC is no longer needed. In the CentOS 7 era you had to write DHCPDARGS=eth0 in /etc/sysconfig/dhcpd; that file is deprecated in Rocky 9 — the comment at the top of the file reads verbatim “This file is NOT used anymore”. The service unit now runs dhcpd -f -cf /etc/dhcp/dhcpd.conf -user dhcpd -group dhcpd --no-pid $DHCPDARGS directly, and the listening scope is decided entirely by dhcpd.conf.

I tested both sides of this rule. When no interface address fell inside any declared subnet, dhcpd refused to start outright; after assigning 192.168.254.2/24 to the interface and starting again, it bound normally:

Figure 3

So on the server you only need to make sure the interface has a static address in the target subnet, then start the service as usual:

1
2
systemctl enable --now dhcpd
systemctl status dhcpd

systemctl status dhcpd showing active (running) means the startup succeeded; journalctl -u dhcpd shows the line Listening on LPF/eth0/<MAC>/192.168.254.0/24 — that line confirms the server has bound to the interface and subnet.

Note: there is no systemd inside the container, so the listening verification was done equivalently with dhcpd -d -f in foreground debug mode (that is Figure 3); the systemctl usage matches the dhcpd.service unit bundled with Rocky 9, whose unit file contents were checked inside the container.

If the status is failed, run dhcpd -t first to rule out a syntax problem, then look at the specific error line in the journal.

Configure the Firewall

If firewalld is enabled, the DHCP service must be allowed through, otherwise the clients’ broadcast requests never reach the server:

1
2
firewall-cmd --permanent --add-service=dhcp
firewall-cmd --reload

After running these, the output of firewall-cmd --list-services should include dhcp. The dhcp service definition (allowing UDP 67/68) ships with the firewalld package on Rocky 9, at /usr/lib/firewalld/services/dhcp.xml — I confirmed that file exists inside the container.

Note: firewalld was not running inside the container (it depends on systemd), so the two commands above were not executed there; the source is standard firewalld usage plus the bundled dhcp.xml service definition.

Client-Side Verification

Switch the client’s NIC to DHCP and confirm two things:

  1. The IP an ordinary device gets falls within range 192.168.254.151 - 192.168.254.200 — in testing, a client whose MAC was not in the binding table received 192.168.254.151, exactly the first free address in the range;
  2. The device with MAC 00:11:22:33:44:55 always receives 192.168.254.150, unchanged across reboots and lease renewals.

On a Linux client, dhclient lets you watch the full negotiation:

1
dhclient -v eth0

The measured log of a binding hit is below: the server’s OFFER/ACK both go to 00:11:22:33:44:55, and the client ends with bound to 192.168.254.150:

Figure 4

The four-step negotiation is DHCPDISCOVER → DHCPOFFER → DHCPREQUEST → DHCPACK in order; once bound to <IP> appears in the client log, it worked. In this verification the server and the client ran in the same container, each side seeing the other’s packets through the kernel’s receive path, and the logs match up one to one.

Differences from Older Versions (CentOS 7)

For readers migrating from CentOS 7, the changes concentrate in three places:

Item CentOS 7 Rocky Linux 9
Install yum install dhcp (ISC DHCP 4.2.5) dnf install dhcp-server (4.4.2)
Specify listening NIC Write DHCPDARGS=eth0 in /etc/sysconfig/dhcpd File deprecated; dhcpd picks interfaces automatically from subnet declarations
Software source After EOL, switch to vault.centos.org Official repositories actively maintained

The dhcpd.conf syntax itself works on both versions; migrating is mostly a matter of swapping the install command and deleting DHCPDARGS.

Common Errors

  • No subnet declaration for eth0 / Not configured to listen on any interfaces!: no interface address falls inside a declared subnet, so dhcpd refuses to start. Assign the interface an address in a subnet declared in dhcpd.conf (Figure 3 shows the before/after contrast).
  • On CentOS 7, No package dhcp available or yum returning 403: CentOS 7 is EOL and its official repositories are offline. Comment out mirrorlist in the repo file, point baseurl at http://vault.centos.org, then yum clean all and reinstall. vault occasionally rate-limits the altarch (ARM) paths — on a 403, retry later or switch to an x86_64 machine.
  • Client gets no address but the server looks fine: nine times out of ten the firewall isn’t allowing UDP 67, or a second DHCP server in the subnet is racing to answer.
  • The binding doesn’t take effect and the device still gets a dynamic-pool address: the MAC is written wrong (the format must be 00:11:22:33:44:55, colon-separated lowercase), or the fixed-address sits inside the range and was dynamically handed to another device first — this scenario was reproduced in the container: with the range covering 150, a non-matching client grabbed 150 first.

Notes and Cautions

  • Back up dhcpd.conf before editing it; a broken configuration keeps dhcpd from starting, but the errors from dhcpd -t and journalctl -u dhcpd pinpoint the line.
  • default-lease-time is the default lease in seconds and max-lease-time is the lease ceiling — 600 and 7200 seconds in this post. On networks with many mobile devices, shortening the default lease speeds up address recycling.
  • The bound fixed address must be inside the subnet declared in the config and must avoid the range’s dynamic allocation space (here the range starts at 151, leaving 150 for the bound address). Dynamic allocation only looks at free slots inside the range and never consults the host table — overlap means a conflict sooner or later.
  • host declarations take effect globally (that is exactly what the syntax-check WARNING is about); on multi-subnet deployments, write all host blocks at the top level of the configuration file.
  • Rollback plan: systemctl stop dhcpd && systemctl disable dhcpd takes the service fully offline; if the configuration gets mangled, restore the .backup made at the start.

For handing out addresses automatically on an intranet, a single dhcpd configuration file covers it all: a dynamic pool for ordinary devices, host bindings for devices that “must have a fixed IP”. Remember two things when deploying — never let the bound address overlap the range, and put host blocks at the top level for multi-subnet setups. Add Rocky 9’s new twist that “DHCPDARGS is no longer needed”, and everything else is routine operations straight from the configuration file.

This article was rebuilt from the author’s CSDN blog posts published between 2020 and 2024, originally published on CSDN.