Skip to content

Instantly share code, notes, and snippets.

@ccy
Last active December 12, 2020 06:43
Show Gist options
  • Save ccy/c0ade259ae89b769de095f70a63557f7 to your computer and use it in GitHub Desktop.
Save ccy/c0ade259ae89b769de095f70a63557f7 to your computer and use it in GitHub Desktop.
Self assign DHCP lease from own DHCP server
- service_facts:
- name: Install dhclient service file
become: yes
copy: src=dhclient@.service dest=/etc/systemd/system/
- name: Enable dhclient@{{ ansible_facts.default_ipv4.interface }}.service
become: yes
systemd:
name: dhclient@{{ ansible_facts.default_ipv4.interface }}
state: restarted
enabled: yes
daemon_reload: yes
[Unit]
Description=Get DHCP lease on %I
Documentation=man:dhclient(8)
Wants=network.target
#Before=network.target
After=isc-dhcp-server.service
BindsTo=sys-subsystem-net-devices-%i.device
After=sys-subsystem-net-devices-%i.device
[Service]
Type=simple
# Flush IP address on interface %I
ExecStartPre=ip a flush dev %I
# Force dhclient to run in foreground and let systemd handle all Unix FDs (0,1, and 2)
ExecStart=/sbin/dhclient -4 -d -v -cf /etc/dhcp/dhclient.%I.conf -pf /var/run/dhclient.%I.pid -lf /var/lib/dhcp/dhclient.%I.leases %I
PIDFile=/var/run/dhclient.%I.pid
# DHCLIENT should never exit, ever.
# DHCLIENT should not use '-1' option nor handle exit code 2 in a special way
Restart=always
[Install]
WantedBy=multi-user.target

Introduction

A DHCP server listen on a network interface (e.g: eth0)

$ cat /etc/default/isc-dhcp-server
INTERFACESv4="eth0"
INTERFACESv6=""

shall use a unique static IP address:

$ cat /etc/network/interfaces
auto eth0
allow-hotplug eth0
iface eth0 inet static
  address 192.168.1.1
  netmask 255.255.255.0
  gateway 192.168.1.254

The eth0 with static IP address shall have forever lease time:

$ ip a show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:15:5d:dc:a1:1b brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.1/24 brd 192.168.1.255 scope global global eth0
       valid_lft forever preferred_lft forever

Lastly, setup the DNS resolver to make WAN requests works properly:

$ cat /etc/resolv.conf
domain example.com
search example.com
nameserver 192.168.1.254

Now, the essential network configuration of the DHCP host is complete.

Problem

A DHCP lease may contain useful information like routers, subnet-mask, domain-name-servers, domain-name, ntp-servers and other options. These options stores in /etc/dhcp/dhcpd.conf by default. However, a static IP address host can't utilize this information via DHCP lease request. It shall populate in various config files. Any changes to the configuration may update both host config files and dhcpd.conf.

Is there a way to make the dhcpd.conf to act as a single source of truth for DHCP lease information by forcing a dhclient service to obtain DHCP lease from DHCP service listen to same interface? If that is possible, perhaps we can simplify the configuration steps.

Obtain DHCP lease from own DHCP server

Define a static IP address for network interface

This is require or else dhcp server won't start.

$ cat /etc/network/interfaces
auto eth0
allow-hotplug eth0
iface eth0 inet static
  address 192.168.1.1/24

Optional: Define a fixed ip address in DHCP client configuration

This step is optional unless you want the IP address for your DHCP host.

$ cat /etc/dhcp/dhclient.conf
option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;

send host-name = gethostname();
request subnet-mask, broadcast-address, time-offset, routers,
        domain-name, domain-name-servers, domain-search, host-name,
        dhcp6.name-servers, dhcp6.domain-search, dhcp6.fqdn, dhcp6.sntp-servers,
        netbios-name-servers, netbios-scope, interface-mtu,
        rfc3442-classless-static-routes, ntp-servers;

interface "eth0" {
  send dhcp-requested-address 192.168.1.1;
}

Define a DHCP pool range for the network

$ cat /etc/dhcp/dhcpd.conf
subnet 192.168.1.0 netmask 255.255.255.0 {
  option domain-name "example.lan";
  option subnet-mask 255.255.255.0;
  option routers 192.168.1.254;
  option domain-name-servers 192.168.1.253;

  pool {
    range 192.168.1.10 192.168.1.200;
  }
}

Restart the machine and check you have a static IP address and dhcp server running. Now execute these to get IP address from DHCP server:

# Flush the IP address of interface eth0
$ ip a flush dev eth0

# Remove dhclient lease
$ rm /var/lib/dhcp/dhclient.*

# Start dhclient for interface eth0
dhclient -v eth0

A new IP address shall get allocate for the interface eth0 with DHCP requested options.

Make dhclient as systemd service

The above method works by start dhclient on interface eth0 manually. The self obtain DHCP lease mechanism won't survive after reboot. Let's write a systemd service. The systemd service must only start after isc-dhcp-server. A sample dhclient@.service and ansible script are available to show how it works.

After deploy dhclient@.service file with ansible script. The dhclient service will always start and obtain DHCP lease after reboot.

Losing IP address when lease expired

The above configuration works with a potential issue - IP address may suddenly lost due to the lease didn't renew after lease expire. During DHCP lease RENEW state, the DHCP server does not acknowledge DHCP Request from same interface:

DHCPREQUEST for 192.168.1.5 on eth0 to 192.168.1.1 port 67
DHCPREQUEST for 192.168.1.5 on eth0 to 192.168.1.1 port 67
DHCPREQUEST for 192.168.1.5 on eth0 to 192.168.1.1 port 67

The consequences of losing IP address is whole network service down once all leases expired. Fortunately, this issue can be solved with failover peer deployment strategy.

Deploy self assigned DHCP services with failover peer strategy

When DHCP discover packet broadcast to DHCP failover cluster, the DHCP service always offer dhcp-server-identifier as IP address of primary DHCP (not secondary DHCP). In this case, both primary and secondary DHCP host shall receive dhcp-server-identifier as primary DHCP IP address. When dhclient attempt to renew, primary DHCP host will fail to renew lease due to DHCP request to same interface. Only DHCP request from secondary DHCP success.

Apparently, in order to make self assigned DHCP service works, we can force dhlient acquire and renew lease from it's peer.

Acquire DHCP lease from peer

Primary DHCP host - 192.168.1.1:/etc/dhcp/dhclient.eth0.conf:

ExecStart=/sbin/dhclient -4 -d -v -cf /etc/dhcp/dhclient.%I.conf -s 192.168.1.2 -pf /var/run/dhclient.%I.pid -lf /var/lib/dhcp/dhclient.%I.leases %I

Secondary DHCP host - 192.168.1.2:/etc/dhcp/dhclient.eth0.conf:

ExecStart=/sbin/dhclient -4 -d -v -cf /etc/dhcp/dhclient.%I.conf -s 192.168.1.1 -pf /var/run/dhclient.%I.pid -lf /var/lib/dhcp/dhclient.%I.leases %I

Force renew lease from peer

To force dbclient renew from peer:

Primay Host - 192.168.1.1:/etc/dhcp/dhclient.eth0.conf

supersede dhcp-server-identifier 192.168.1.2

Secondary Host - 192.168.1.2:/etc/dhcp/dhclient.eth0.conf

supersede dhcp-server-identifier 192.168.1.1

After start the dhclient@eth0 service, secondary IP address appears as follow:

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:15:5d:dc:a1:1b brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.1/24 brd 192.168.255.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet 192.168.1.20/24 brd 192.168.255.255 scope global secondary eth0
       valid_lft forever preferred_lft forever

This setup shall never fail until one of the peer is down.

Losing IP address still happen when failover peer down

Till now, the self obtain IP address works fluently until one of the peer down. The IP address obtained from DHCP lease will eventually expire and a much worst situation happen - losing both IP addresses. This is due to dhclient will flush all IP addresses of network interface once lease expire. This cause the DHCP host become unreachable.

We can try to solve the issue with

  1. Forever lease time that never expire. Lease stay forever even if peer host is down.
  2. Optional: supersede dhcp-renewal-time to obtain new lease options in timely fashion (e.g.: 3600 seconds) to utilize DHCP service as usual.

This can be done with:

$ cat /etc/dhcp/dhclient.eth0.conf
...
supersede dhcp-renewal-time 3600; // 1 hour
supersede dhcp-rebinding-time -1; // forever
supersede dhcp-lease-time -1;     // forever

Most configurations are done so far, there is one more step to do before start or enable the self assign DHCP service. If there are left over lease file: e.g.: /var/lib/dhcp/dhclient.eth0.leases. Remove the lease file before start the dhclient service. This is to make sure the interface e.g.: eth0 always obtain a new DHCP lease as secondary IP address. Failure to do so will cause dhclient flush the original static IP address.

Now, start or enable the service as usual:

$ systemctl enable dhclient@eth0
$ systemctl start dhclient@eth0

If a failover peer host down, the DHCP lease will never expire. The dhclient service will keep sending DHCPRequest (shown in /var/log/syslog) until the failover peer back online.

Deprecated solution: Configure secondary IP address for DHCP network interface

The IP address losing issue may solve with 2 IP addresses strategy for the network interface.

First, move the fixed IP address to secondary slot and leave the primary slot as manual:

$ cat /etc/network/interfaces
auto eth0
allow-hotplug eth0
iface eth0 inet manual

auto eth0:1
allow-hotplug eth0:1
iface eth0:1 inet static
  address 10.2.0.255
  netmask 255.255.0.0
  gateway 10.2.0.1

Next, configure isc-dhcp-server listen to interface eth0:1:

$ cat /etc/default/isc-dhcp-server
INTERFACESv4="eth0:1"
INTERFACESv6=""

Add a dhclient configuration for interface eth0:1:

$ cat /etc/dhcp/dhclient.eth0\:1.conf
option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;

send host-name = gethostname();
request subnet-mask, broadcast-address, time-offset, routers,
        domain-name, domain-name-servers, domain-search, host-name,
        dhcp6.name-servers, dhcp6.domain-search, dhcp6.fqdn, dhcp6.sntp-servers,
        netbios-name-servers, netbios-scope, interface-mtu,
        rfc3442-classless-static-routes, ntp-servers;

The dhclient on eth0 will flush ip address of eth0:1, it shall bring back to online with this script

$ cat /etc/dhcp/dhclient-enter-hooks.d/down-eth0\:1
ifdown eth0:1

$ chmod +x /etc/dhcp/dhclient-enter-hooks.d/down-eth0\:1

$ cat /etc/dhcp/dhclient-exit-hooks.d/up-eth0\:1
ifup eth0:1

$ chmod +x /etc/dhcp/dhclient-exit-hooks.d/up-eth0\:1

Finally, enable dhclient@eth0:1 service and reboot the host for the changes to take effect:

$ sudo systemctl enable dhclient@eth0:1
$ sudo reboot

After reboot, the eth0 interface shall have these IP addresses:

$ ip a show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:15:5d:dc:a1:1b brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.110/24 brd 192.168.255.255 scope global dynamic eth0
       valid_lft 60sec preferred_lft 60sec
    inet 192.168.1.1/24 brd 192.168.255.255 scope global secondary eth0:1
       valid_lft forever preferred_lft forever

Conclusion

While the solution may work to self obtain DHCP lease from same interface. It is not advisable to deploy DHCP server in such manner. It is kind of chicken and egg problem. Stay with static IP address deployment strategy is safer for serious production environment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment