Reliably detect all DHCP servers on a network?

Just to be sure, this?

@DL6ER Can there be a pihole shortcut for this? It seems to be a very helpful thing.

@deHakkelaar Why the -e eth0 setting? What happens if you omit this and a machine has multiple active interfaces?

There could be. However, nmap is not a dependency of Pi-hole and I wouldn't want to add it for the very few cases where this may be used. It would make more sense to write a cheat-sheet or something similar and put it on there.

I noticed by doing a tcpdump, if I omit the interface argument -e eth0, nmap will broadcast a DHCPDISCOVER on one (connected) interface only and not both.
Most of the times, being specifyk is a good thing :wink:

I agree with @DL6ER.
Another dependencie with its own dependencies.

Another snag I noticed, the broadcast-dhcp-discover script doesnt report anything with nmap version =< 7.40 (Debian Stretch for example).
I tested this by installing the nmap v7.70 Buster version on a Pi of mine running Stretch.

EDIT: If below could be ported magically somehow :wink:
Would be nice addition to the debugger.

Could be done, but where will we find the time?...

You may have to run that a few times in short succession, or from two consoles on the same machine at the same time: That broadcast-dhcp-discover script is a bit unreliable, as it just returns the very first DHCP answer it receives.

And if I read the code from that GitHub link correctly, then that fix addresses multiple interfaces, but still processes only the first answer received via each interface.

On Raspberry Pi OS/Debian systems, sudo dhcpcd -T will yield similar results without having to install nmap - unfortunately in the same arbitrary first answer accepted fashion. :wink:

Yeah the "Response 1 of 1:" line threw me off thinking it will list all responses ... assumptions again :wink:

My Debian laptop is running NetworkManager and not dhcpcd5.
And I believe that one doesnt broadcast anything if you configured a static IP & DNS like in case of Pi-hole:

pi@ph5:~ $ man dhcpcd
[..]
     -T, --test
             On receipt of DHCP messages just call
             /lib/dhcpcd/dhcpcd-run-hooks with the reason of TEST
             which echos the DHCP variables found in the message to
             the console.  The interface configuration isn't touched
             and neither are any configuration files.  The
             rapid_commit option is not sent in TEST mode so that the
             server does not lease an address.  To test INFORM the in‐
             terface needs to be configured with the desired address
             before starting dhcpcd.

I flipped on all three DHCP services that I have available, my router and two (wired) Pi-hole instances to run some tests.
My client laptop connected through WiFi only reported my PH v5 node @10.0.0.4 :

dehakkelaar@laptop:~$ sudo nmap -e wlan0 --script broadcast-dhcp-discover
[..]
|     Server Identifier: 10.0.0.4

Above is consistent on another wired client:

xbian@avr ~ $ sudo nmap -e eth0 --script broadcast-dhcp-discover
[..]
|     Server Identifier: 10.0.0.4

My PH v5 host reports only itself:

pi@ph5:~ $ sudo nmap -e eth0 --script broadcast-dhcp-discover
[..]
|     Server Identifier: 10.0.0.4

And my PH v4 host wont report a thing bc its still on nmap v7.40.
Reluctant to install nmap v7.70 bc I have to stir up libc.so.6 version dependency which I wont do on this live system.

So the broadcast-dhcp-discover script is not that reliable to investigate DHCP servers active on the network.
As alternative, you could do a:

dehakkelaar@laptop:~$ sudo nmap -sU -p67 --open 10.0.0.0/24
Starting Nmap 7.70 ( https://nmap.org ) at 2020-09-04 02:40 CEST
Nmap scan report for 10.0.0.1
Host is up (0.00089s latency).

PORT   STATE         SERVICE
67/udp open|filtered dhcps
MAC Address: 50:46:5D:xx:xx:xx (Asustek Computer)

Nmap scan report for noads.dehakkelaar.nl (10.0.0.2)
Host is up (0.0015s latency).

PORT   STATE         SERVICE
67/udp open|filtered dhcps
MAC Address: B8:27:EB:xx:xx:xx (Raspberry Pi Foundation)

Nmap scan report for ph5.dehakkelaar.nl (10.0.0.4)
Host is up (0.0025s latency).

PORT   STATE         SERVICE
67/udp open|filtered dhcps
MAC Address: B8:27:EB:xx:xx:xx (Raspberry Pi Foundation)

Nmap done: 256 IP addresses (8 hosts up) scanned in 5.13 seconds

And instead of doing a broadcast, do a unicast dhcp-discover on the IP's:

dehakkelaar@laptop:~$ sudo nmap -sU -p67 --script dhcp-discover 10.0.0.2
Starting Nmap 7.70 ( https://nmap.org ) at 2020-09-04 02:36 CEST
Nmap scan report for noads.dehakkelaar.nl (10.0.0.2)
Host is up (0.0011s latency).

PORT   STATE SERVICE
67/udp open  dhcps
| dhcp-discover:
|   DHCP Message Type: DHCPACK
|   Server Identifier: 10.0.0.2
|   IP Address Lease Time: 23h42m03s
|   Subnet Mask: 255.255.255.0
|   Broadcast Address: 10.0.0.255
|   Domain Name Server: 10.0.0.2
|   Domain Name: dehakkelaar.nl
|_  Router: 10.0.0.1
MAC Address: B8:27:EB:xx:xx:xx (Raspberry Pi Foundation)

Nmap done: 1 IP address (1 host up) scanned in 0.95 seconds

If want to be sure what DHCP response gets in first on a client, you'd have to run nmap with broadcast-dhcp-discover on that client.
But it still does show other useful data like DNS servers, gateway etc advertised via DHCP.

This almost convinced me that this is a valuable thing to add to the debugger, however, I found some strange results in my own local network. Maybe you have suggestions on how to improve the automated scanning.

  1. Step: Get address ranges of all interfaces
    (for simplicity, I only the devices attached to eth2 which represents my local home network)

     $ ip a
     [...]
     3: eth2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
         link/ether X brd ff:ff:ff:ff:ff:ff
         inet 192.168.2.223/24 brd 192.168.2.255 scope global dynamic noprefixroute eth2
            valid_lft 81690sec preferred_lft 81690sec
     [...]
    
  2. Step: Scan UDP ports 67 on this network:

    $ sudo nmap -sU -p67 --open 192.168.2.223/24
    
    Starting Nmap 7.60 ( https://nmap.org ) at 2020-09-04 09:47 CEST
    Nmap scan report for pi.hole (192.168.2.10)
    Host is up (0.00037s latency).
    
    PORT   STATE         SERVICE
    67/udp open|filtered dhcps
    MAC Address: X (Hewlett Packard)
    
    Nmap scan report for webradio.lan (192.168.2.239)
    Host is up (0.15s latency).
    
    PORT   STATE         SERVICE
    67/udp open|filtered dhcps
    MAC Address: X (Frontier Silicon)
    
    Nmap scan report for android.lan (192.168.2.244)
    Host is up (0.25s latency).
    
    PORT   STATE         SERVICE
    67/udp open|filtered dhcps
    MAC Address: X (Unknown)
    
    Nmap done: 256 IP addresses (6 hosts up) scanned in 8.05 seconds
    

    This result is a bit unexpected: Obviously, only the pi.hole host actually hosts a DHCP server, the others are likely just dropping all incoming connections. Something to watch out for in the following

  3. Step: Scan each of the reported hosts using the dhcp-discover script:

    $ sudo nmap -sU -p67 --script dhcp-discover 192.168.2.10
    
    Starting Nmap 7.60 ( https://nmap.org ) at 2020-09-04 09:56 CEST
    Nmap scan report for pi.hole (192.168.2.10)
    Host is up (0.00029s latency).
    
    PORT   STATE SERVICE
    67/udp open  dhcps
    | dhcp-discover: 
    |   DHCP Message Type: DHCPACK
    |   Server Identifier: 192.168.2.10
    |   IP Address Lease Time: 22h31m50s
    |   Subnet Mask: 255.255.255.0
    |   Broadcast Address: 192.168.2.255
    |   Domain Name Server: 192.168.2.10
    |   Domain Name: lan
    |_  Router: 192.168.2.1
    MAC Address: X (Hewlett Packard)
    
    Nmap done: 1 IP address (1 host up) scanned in 0.76 seconds
    

    So this worked, great.

    $ sudo nmap -sU -p67 --script dhcp-discover 192.168.2.239
    
    Starting Nmap 7.60 ( https://nmap.org ) at 2020-09-04 09:56 CEST
    Nmap scan report for Internet-Radio.lan (192.168.2.239)
    Host is up (0.088s latency).
    
    PORT   STATE         SERVICE
    67/udp open|filtered dhcps
    MAC Address: X (Frontier Silicon)
    
    Nmap done: 1 IP address (1 host up) scanned in 6.40 seconds
    

    False positive, acknowledged.

    $ sudo nmap -sU -p67 --script dhcp-discover 192.168.2.244
    
    Starting Nmap 7.60 ( https://nmap.org ) at 2020-09-04 09:57 CEST
    Nmap scan report for dominik-android.lan (192.168.2.244)
    Host is up (0.085s latency).
    
    PORT   STATE  SERVICE
    67/udp closed dhcps
    MAC Address: X (Unknown)
    
    Nmap done: 1 IP address (1 host up) scanned in 0.94 seconds
    

    False positive, acknowledged.

So the idea is now to run step 1, extract all possible subnets (most of the time there will be only one, but you never know for sure). Then run steps 2 for all extracted subnets and step 3 for all extracted subnets * detected devices (may contain a lot of false-positives).

As much as I like the idea of adding this to the debugger, the entire process took quite some time (roughly 10 seconds even if scripted) with only one scanned interface due to the false positives may taking a long time to respond. So I'd rather favor what I dismissed above, to really add a pihole scandhcp (or similar) so it can be done easily on request of a user.

I see that the nmap scripting engine (NSE) is written in Lua (pinging @DanSchaper just for awareness), so it may be worthwhile to explore a variant of implementing all the three points I mentioned above directly in the script to avoid complex bash scripting around them.

1 Like

In the end it turned out to be easier - and more reliable if you want to receive multiple offers - to implement this completely ourselves. No added dependencies anywhere.

Example:

$ sudo pihole-FTL dhcp-discover

Sending DHCPDISCOVER on interface lo ...
 Nobody replied to our request on this interface

Sending DHCPDISCOVER on interface eth1 ...
 Nobody replied to our request on this interface

Sending DHCPDISCOVER on interface eth2 ...
 Received 300 bytes from 192.168.2.10
  Offered IP address: 192.168.2.223
  Server IP address: 192.168.2.10
  Relay-agent IP address: N/A
  DHCP options:
   Message type: DHCPOFFER
   Server identification: 192.168.2.10
   Lease time: 86400 (1d 0h 0m 0s)
   Renewal time: 43200 (12h 0m 0s)
   Rebindung time: 75600 (21h 0m 0s)
   Subnet mask: 255.255.255.0
   Broadcast address: 192.168.2.255
   DNS server 1: 192.168.2.10
   Domain name: "lan"
   Router 1: 192.168.2.1
   --- end of options ---

Sending DHCPDISCOVER on interface lxcbr0
 Received 300 bytes from 10.0.3.1
  Offered IP address: 10.0.3.16
  Server IP address: 10.0.3.1
  Relay-agent IP address: N/A
  DHCP options:
   Message type: DHCPOFFER
   Server identification: 10.0.3.1
   Lease time: 3600 (1h 0m 0s)
   Renewal time: 1800 (30m 0s)
   Rebindung time: 3150 (52m 30s)
   Subnet mask: 255.255.255.0
   Broadcast address: 10.0.3.255
   Router 1: 10.0.3.1
   DNS server 1: 10.0.3.1
   --- end of options ---

The timeout until we wait for offers is currently hard-coded to two seconds.

Please try it yourself!
I am, as always, very interested in your opinion and suggestions for further improvements:

pihole checkout ftl new/dhcp-discover
2 Likes

This. Is. Awesome. Absolutely crazy!!!

You guys rock. You just wrote over 650 lines of complex C code in just two hours! F.U.C.K.!

Its MAD crazy how quick :smiley:
I had partial success while trying out below outdated PHP5 code:

https://www.phpclasses.org/package/6621-PHP-Send-queries-to-a-DHCP-server.html#view_files

I got it sort of to send but in the end, too many things had to be ported to PHP7

This is truly amazing :+1:
Thanx allot!

EDIT:

pi@noads:~ $ tail -f /var/log/pihole.log | grep dnsmasq-dhcp
Sep  4 13:30:13 dnsmasq-dhcp[7056]: DHCPDISCOVER(eth0) b8:27:eb:xx:xx:xx
Sep  4 13:30:13 dnsmasq-dhcp[7056]: DHCPOFFER(eth0) 10.0.0.109 b8:27:eb:xx:xx:xx

EDIT2: @DL6ER , can you send a DHCPINFORM as well instead of a DHCPDISCOVER ?

Some feedback,
I noticed if I flip on all three DHCP servers again, its same old race condition again who is quickest responding (not the own host at 10.0.0.4 it seems):

pi@ph5:~ $ pihole-FTL dhcp-discover
Sending DHCPDISCOVER on interface lo ...
 Nobody replied to our request on this interface

Sending DHCPDISCOVER on interface eth0 ...
 Received 303 bytes from 10.0.0.2
  Offered IP address: 10.0.0.109
  Server IP address: 10.0.0.2
  Relay-agent IP address: N/A
  DHCP options:
   Message type: DHCPOFFER
   Server identification: 10.0.0.2
   Lease time: 86400 (1d 0h 0m 0s)
   Renewal time: 43200 (12h 0m 0s)
   Rebindung time: 75600 (21h 0m 0s)
   Subnet mask: 255.255.255.0
   Broadcast address: 10.0.0.255
   DNS server 1: 10.0.0.2
   Domain name: "dehakkelaar.nl"
   Router 1: 10.0.0.1
   --- end of options ---

And when supply IP argument, I get to see two of three active DHCP servers:

pi@ph5:~ $ pihole-FTL dhcp-discover 10.0.0.1
Sending DHCPDISCOVER on interface lo ...
 Nobody replied to our request on this interface

Sending DHCPDISCOVER on interface eth0 ...
 Received 312 bytes from 10.0.0.1
  Offered IP address: 10.0.0.110
  Server IP address: 10.0.0.1
  Relay-agent IP address: N/A
  DHCP options:
   Message type: DHCPOFFER
   Server identification: 10.0.0.1
   Lease time: 86400 (1d 0h 0m 0s)
   Renewal time: 43200 (12h 0m 0s)
   Rebindung time: 75600 (21h 0m 0s)
   Subnet mask: 255.255.255.0
   Broadcast address: 10.0.0.255
   DNS server 1: 10.0.0.1
   Unknown option 252 with length 1
   Unknown option 44 with length 4
   Domain name: "dehakkelaar.nl"
   Router 1: 10.0.0.1
   --- end of options ---
 Received 303 bytes from 10.0.0.2
  Offered IP address: 10.0.0.109
  Server IP address: 10.0.0.2
  Relay-agent IP address: N/A
  DHCP options:
   Message type: DHCPOFFER
   Server identification: 10.0.0.2
   Lease time: 86400 (1d 0h 0m 0s)
   Renewal time: 43200 (12h 0m 0s)
   Rebindung time: 75600 (21h 0m 0s)
   Subnet mask: 255.255.255.0
   Broadcast address: 10.0.0.255
   DNS server 1: 10.0.0.2
   Domain name: "dehakkelaar.nl"
   Router 1: 10.0.0.1
   --- end of options ---

And trying to discovering the own host @10.0.0.4, I get to see reply from my other Pi again @10.0.0.2:

pi@ph5:~ $ pihole-FTL dhcp-discover 10.0.0.4
Sending DHCPDISCOVER on interface lo ...
 Nobody replied to our request on this interface

Sending DHCPDISCOVER on interface eth0 ...
 Received 303 bytes from 10.0.0.2
  Offered IP address: 10.0.0.109
  Server IP address: 10.0.0.2
  Relay-agent IP address: N/A
  DHCP options:
   Message type: DHCPOFFER
   Server identification: 10.0.0.2
   Lease time: 86400 (1d 0h 0m 0s)
   Renewal time: 43200 (12h 0m 0s)
   Rebindung time: 75600 (21h 0m 0s)
   Subnet mask: 255.255.255.0
   Broadcast address: 10.0.0.255
   DNS server 1: 10.0.0.2
   Domain name: "dehakkelaar.nl"
   Router 1: 10.0.0.1
   --- end of options ---

EDIT:

Rebindung lost in translation :wink:

There are no arguments for this command... So we got different results for one and the same test. We pick up all valid packets that arrive at the interface. I'll have to set up a few DHCP servers when back home Monday.

Not sure if this is really a race collision on the wire (we can't do anything about this, then) or if we maybe just have to extend the timeout from two seconds to something larger.

Not sure what you want, "as well" or "instead of"? As well wouldn't make much/any difference. "Instead" seems wrong, isn't the DHCPINFORM meant to be send to specific addresses instead of the broadcast.

The most recent DHCPv4 Standard [RFC2131] added a new DHCPv4 message: DHCPINFORM. The intent of the DHCPINFORM message was for clients that used manually entered fixed IPv4 addresses to still be able to get some configuration state dynamically.

We likely also want to see if a client would obtain a proper address from the server.

Yeah I meant instead of.
About DHCPINFORM:

DHCPInform Message

DHCPInform is a new DHCP message type, defined in RFC 2131. DHCPInform is used by DHCP clients to obtain DHCP options.

https://www.omnisecu.com/tcpip/dhcp-dynamic-host-configuration-protocol-messages.php

And I notice just now its doing broadcast and not unicast so no IP arguments:

EDIT:

Your right:

DHCPINFORM - Client to server, asking only for local configuration
parameters; client already has externally configured
network address.

https://www.ietf.org/rfc/rfc2131.txt

Yeah maybe extend the timeout a little bit.
When have all three active, sometimes I get report from only one DHCP server and sometimes two but never the own host.
I recon that comes with broadcasting not to your own IP.

Yes, that's also why you would not get an IP address from your own host. I can see some value in testing if the interface hosts an DHCP server on the device itself.

This adds more and more tests and with an increased timeout this may become unhandy. I will experiment with parallelizing all the requests. Should be possible, may only a bit harder to implement. When we can do everything at once we can easily test the local host as well plus grant ourselves a comfortable timeout of maybe even up to 10 seconds.

1 Like

The scan is now fully-multithreaded and the global timeout is 10 seconds. I have a lack of local DHCP servers in my hotel WiFi, however, I set up a local dnsmasq on my laptop and the scan picked up both the AP's DHCP server as well as my new local one. I've seen that the local dnsmasq DHCP often (but not always!) needs a few seconds until it replies.

@deHakkelaar This may explain why you've seen sometimes one, sometimes more DHCP servers. Please try again.

I've also found that my local dnsmasq very well responds to broadcast messages, so no need to send to the interface's local address in my case:

dnsmasq-dhcp: DHCPDISCOVER(wlp4s0) xx:xx:xx:xx:xx:xx 
dnsmasq-dhcp: DHCPOFFER(wlp4s0) 192.168.3.191 xx:xx:xx:xx:xx:xx 
dnsmasq-dhcp: no address range available for DHCP request via lo
dnsmasq-dhcp: DHCP packet received on enp0s25 which has no address

You can see here that dnsmasq received requests on all three interfaces my laptop has (we also try to discover on unconfigured interfaces!). However, it is not configured to serve addresses in the 127.0.0.1/8 range, and there is no cable in enp0s25 so this interface is completely down. Everything seems 100% correct here.

edit:

I also added these for you. I picked what my DHCP servers do, so far, because I didn't feel like adding all the 200+ possible options which may never be seen in the wild.

1 Like

This is really a very useful tool.

Yeah I noticed with time nmap with unicast, mine all need about 3.5 seconds to respond.
A very sleek feature especially bc the own host gets reported as well which nmap doesnt do when broadcasting.
Troubleshooting DHCP has gotten allot easier.
Amazing job !

pi@ph5:~ $ pihole-FTL dhcp-discover
Scanning all your interfaces for DHCP servers
Timeout: 10 seconds

* Received 303 bytes from eth0:10.0.0.4
  Offered IP address: 10.0.0.195
  Server IP address: 10.0.0.4
  Relay-agent IP address: N/A
  DHCP options:
   Message type: DHCPOFFER
   Server identification: 10.0.0.4
   Lease time:1d (86400 seconds)
   Renewal time:12h (43200 seconds)
   Rebinding time:21h (75600 seconds)
   Subnet mask: 255.255.255.0
   Broadcast address: 10.0.0.255
   DNS server 1: 10.0.0.4
   Domain name: "dehakkelaar.nl"
   Router 1: 10.0.0.1
   --- end of options ---

* Received 303 bytes from eth0:10.0.0.2
  Offered IP address: 10.0.0.109
  Server IP address: 10.0.0.2
  Relay-agent IP address: N/A
  DHCP options:
   Message type: DHCPOFFER
   Server identification: 10.0.0.2
   Lease time:1d (86400 seconds)
   Renewal time:12h (43200 seconds)
   Rebinding time:21h (75600 seconds)
   Subnet mask: 255.255.255.0
   Broadcast address: 10.0.0.255
   DNS server 1: 10.0.0.2
   Domain name: "dehakkelaar.nl"
   Router 1: 10.0.0.1
   --- end of options ---

* Received 312 bytes from eth0:10.0.0.1
  Offered IP address: 10.0.0.109
  Server IP address: 10.0.0.1
  Relay-agent IP address: N/A
  DHCP options:
   Message type: DHCPOFFER
   Server identification: 10.0.0.1
   Lease time:1d (86400 seconds)
   Renewal time:12h (43200 seconds)
   Rebinding time:21h (75600 seconds)
   Subnet mask: 255.255.255.0
   Broadcast address: 10.0.0.255
   DNS server 1: 10.0.0.1
   WPAD server: "
"
   NetBIOS name server 1: 10.0.0.1
   Domain name: "dehakkelaar.nl"
   Router 1: 10.0.0.1
   --- end of options ---

Ps. those 44 & 252 options are coming from the Asus router which I dont use ... who needs NETBIOS :wink:

EDIT: maybe reporting the IP three times is a bit redundant:

* Received 303 bytes from eth0:10.0.0.2
  Server IP address: 10.0.0.2
  Server identification: 10.0.0.2

Maybe drop the second line ?

This is not very nice, so it presents a simlpe newline as path to the WPAD server. I fixed it.

These lines are the same in your own setup.

They are, in fact, three entirely different items:

  • Received 303 bytes from eth0:10.0.0.2 <--- the address of the sender of the packet
  • Server IP address: 10.0.0.2 <--- the address stored in the field siaddr
  • Server identification: 10.0.0.2 <--- this address is the argument of DHCP option 54

There are many possible scenarios where they may be different, like when you serve a network through a proxy server or use any other kind of NAT-server. We should keep as much information as we have.

2 Likes

This topic was automatically closed 21 days after the last reply. New replies are no longer allowed.