You go here, you go there. Bending DHCP to your will.

TL;DR: How to hand out DNS servers in different orders to different clients based upon MAC address.

Background: I was connected into my office’s VPN a few months ago and was noticed some very slow DNS resolution of host names back at the office. I would attempt to ssh into another host, and the connection would sit there for more than a few seconds before finally proceeding. This didn’t happen for just ssh, but also for making http requests. I dug into my resolv.conf locally and tried sending a few DNS queries via dig to the two DNS servers I was provided. The first one failed, the second one returned immediately with the correct response. I swapped the two entries and DNS resolution locally was back to where I would expect it, very fast. I alerted our IT group and the issue was fixed (the first DNS server had become hung, and needed a process restart).

Curiosity: This got me thinking, was everyone suffering my issue? Did the two DNS servers handed out always come in that same order? If so, DNS would have been slow for everyone. We’d all be timing out trying to query the first server, waiting for our local resolvers to query the second operational server. Could I get a DHCP server to randomize the list of DNS servers to its querying clients?

Assumptions: I am ignoring the fact that our VPN concentrator might not be running the ISC DHCPD, which my examples are based upon. I will split up each DHCP subnet into two groups, in a binary fashion.

How I did it: After doing some Google searches, I came across a post on the mailing list for ISC DHCPD users. It explained that you could do some logic on the incoming MAC address, and based upon that, hand out unique information, among it, DNS, routers, domain names, etc.I was curious to see if I could actually get this working.

I figure the easiest way to do this is paste some config data and go from there.


class "binary-group-0" { match if suffix(binary-to-ascii(2, 8, "", substring(hardware, 6, 1)), 1) = "0"; }  class "binary-group-1" { match if suffix(binary-to-ascii(2, 8, "", substring(hardware, 6, 1)), 1) = "1"; }  subnet netmask { option routers; option domain-name "";  pool { allow members of "binary-group-0"; range; option domain-name-servers,; on commit { execute("/bin/echo", "GROUP ZERO"); } } pool { allow members of "binary-group-1"; range; option domain-name-servers,; on commit { execute("/bin/echo", "GROUP ONE"); } } } 

Configuration explanation:

On lines 2-8, each named class is populated by clients whose MAC address corresponds to the appropriate ‘match’ line. These match do the following: Starting from the 6th byte of the client’s MAC address, grab one byte of data. Once we have that data, convert the binary data to ascii characters, without a separator using base two, each bit of data being eight bits long. With that data, take a string onecharacter from the end. We have created two classes here, one where the last character is 0 (zero), and the other is 1 (one).

On lines 10-30,we have a standard subnet declaration, with two pools. Each pool (lines 14-20, 21-29) uses the ‘allow members of’ to control which class of users from above the pool applies to. In this instance, we hand out two different ranges and sets of domain name servers depending on what class a user belongs. For my own debugging, I stick an ‘on commit’ execution in each pool. This outputs in the log when a lease is acquired for a particular client, and gave me some explanation about where I was in the config. These ‘on commit’ lines are purely for debugging, and can be removed for production. Clients whose MAC address ends in a binary ‘0’, are placed in the range with DNS servers and Those whose MAC address ends in a binary ‘1’ are given an address in the pool, with DNS servers, You could easily put your own DNS servers in this section, modifying the order in any way you please.

Log Output: Using a couple of VM’s on an isolated network at home (and playing with the MAC address of the client), I was able to test my above configuration. Notice on each they are given IP’s from each appropriate range, with the correct ‘echo’ statement being executed.

DHCPDISCOVER from 52:54:00:4c:f3:d4 (testvm1) via re1 DHCPOFFER on to 52:54:00:4c:f3:d4 (testvm1) via re1 execute_statement argv[0] = /bin/echo execute_statement argv[1] = GROUP ZERO GROUP ZERO DHCPREQUEST for ( from 52:54:00:4c:f3:d4 (testvm1) via re1 DHCPACK on to 52:54:00:4c:f3:d4 (testvm1) via re1  DHCPDISCOVER from 52:54:00:4c:f3:d3 via re1 DHCPOFFER on to 52:54:00:4c:f3:d3 (testvm1) via re1 execute_statement argv[0] = /bin/echo execute_statement argv[1] = GROUP ONE GROUP ONE DHCPREQUEST for ( from 52:54:00:4c:f3:d3 (testvm1) via re1 DHCPACK on to 52:54:00:4c:f3:d3 (testvm1) via re1 


For those curious about how I came to breaking up the MAC address of the client, I became painfully familiar with the dhcp-eval man page. I honestly would not wish that man page on anyone, it is woefully confusing for someone who does not dabble in DHCPD configuration on a daily basis.

MAC Address: 52:54:00:4c:f3:d3  binary-to-ascii(2, 8, ":", hardware); 1:1010010:1010100:0:1001100:11110011:11010011  binary-to-ascii(2,8,".", substring(hardware,6,1)); 11010011  suffix(binary-to-ascii(2, 8, ":", hardware), 1); 1 

There you have it. Now you can break up your clients into binary groups, handing them different network information depending on where they fall. Obviously if you want to split them up into more than two groups, the match statements become a bit more verbose for each condition. Ultimately this would not have solved my problem of being given a ‘bad’ DNS server first in line for my request (since my MAC address would always be the same), but it does spread the load among DNS servers over local clients. I am now curious, when I get the free time, to play around with creating an on-commit-like command that based upon its execution (generate a random number for example), changes the order of DNS servers handed out to clients.