<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Manoel Martins</title>
    <description>The latest articles on DEV Community by Manoel Martins (@manoelmartins1980).</description>
    <link>https://dev.to/manoelmartins1980</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4098759%2F376be760-46c9-41ba-83ef-b231d862a2d1.jpg</url>
      <title>DEV Community: Manoel Martins</title>
      <link>https://dev.to/manoelmartins1980</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/manoelmartins1980"/>
    <language>en</language>
    <item>
      <title>Your DHCP server is handing out addresses it never checked</title>
      <dc:creator>Manoel Martins</dc:creator>
      <pubDate>Fri, 11 Sep 2026 22:35:47 +0000</pubDate>
      <link>https://dev.to/manoelmartins1980/your-dhcp-server-is-handing-out-addresses-it-never-checked-aaj</link>
      <guid>https://dev.to/manoelmartins1980/your-dhcp-server-is-handing-out-addresses-it-never-checked-aaj</guid>
      <description>&lt;p&gt;&lt;strong&gt;Tested on Windows Server 2025 (24H2, build 26100.33296)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A machine boots, gets a &lt;code&gt;169.254.x.x&lt;/code&gt; address, and has no network. The DHCP server is up. The service is running. There are no errors in the event log. There are free addresses in the scope. Reboot the client and it works fine.&lt;/p&gt;

&lt;p&gt;I first saw this on Windows Server 2003. I assumed it had been fixed somewhere along the way. It has not — because it was never a bug. It is a default, and the default is still there in Windows Server 2025.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the RFC actually says
&lt;/h2&gt;

&lt;p&gt;RFC 2131 splits address-conflict checking across both ends of the conversation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;the allocating server SHOULD probe the reused address before allocating the address, e.g., with an ICMP echo request, and the client SHOULD probe the newly received address, e.g., with ARP.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two recommendations. Both &lt;code&gt;SHOULD&lt;/code&gt;, neither &lt;code&gt;MUST&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Microsoft implemented both — but ships the server half &lt;strong&gt;turned off&lt;/strong&gt;. The setting is called &lt;em&gt;Conflict detection attempts&lt;/em&gt;, it lives under IPv4 → Properties → Advanced, and its default value is &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lab
&lt;/h2&gt;

&lt;p&gt;Three VMs on a VMware Workstation &lt;strong&gt;LAN Segment&lt;/strong&gt; — a virtual switch with no uplink and no host interface, so nothing here can reach or disturb the physical network:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Address&lt;/th&gt;
&lt;th&gt;MAC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DHCP server (Windows Server 2025)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;172.16.0.2&lt;/code&gt; (static)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;00:0c:29:52:b9:9f&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Squatter (Linux, static, never speaks DHCP)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;172.16.0.10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;00:0c:29:3a:4c:11&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client (Windows 10, &lt;code&gt;WIN10-01&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;DHCP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;00:0c:29:b8:ca:88&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The scope is &lt;code&gt;172.16.0.0/24&lt;/code&gt; with a deliberately tiny range, so that the address I want the server to pick is the only one it can pick. Lease duration: 8 days.&lt;/p&gt;

&lt;p&gt;The squatter simulates what you get from a rogue DHCP server or a hand-configured host inside the scope range: something holding an address the DHCP server believes is free.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test 1 — the default (&lt;code&gt;Conflict detection attempts = 0&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Scope range: &lt;code&gt;172.16.0.10&lt;/code&gt; – &lt;code&gt;172.16.0.10&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;t (s)&lt;/th&gt;
&lt;th&gt;Who&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.000000&lt;/td&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;DISCOVER&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.000406&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;OFFER — &lt;code&gt;172.16.0.10&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.001285&lt;/td&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;REQUEST &lt;code&gt;172.16.0.10&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.003182&lt;/td&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;td&gt;ACK &lt;code&gt;172.16.0.10&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.280785&lt;/td&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;ARP probe: &lt;em&gt;who has &lt;code&gt;172.16.0.10&lt;/code&gt;?&lt;/em&gt; (sender &lt;code&gt;0.0.0.0&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.280908&lt;/td&gt;
&lt;td&gt;Squatter&lt;/td&gt;
&lt;td&gt;ARP reply: &lt;em&gt;&lt;code&gt;172.16.0.10&lt;/code&gt; is at &lt;code&gt;00:0c:29:3a:4c:11&lt;/code&gt;&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.130462&lt;/td&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DHCPDECLINE&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;~7.3–10.3&lt;/td&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;ARP probes for &lt;code&gt;169.254.248.228&lt;/code&gt; (APIPA)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvcanbd6lvaydd3t7lcsn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvcanbd6lvaydd3t7lcsn.png" alt="Wireshark showing DISCOVER, OFFER of 172.16.0.10, REQUEST, ACK, the client's ARP probe, the squatter's reply, and the DHCPDECLINE" width="800" height="162"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The server handed out an occupied address &lt;strong&gt;406 microseconds&lt;/strong&gt; after being asked for one. You do not need to argue about whether it checked: there is no room for a check in 406 µs. The whole capture contains zero ICMP packets and not a single ARP frame originating from the server's MAC.&lt;/p&gt;

&lt;p&gt;Here is that claim as a filter. &lt;code&gt;icmp || (arp &amp;amp;&amp;amp; eth.src == &amp;lt;server MAC&amp;gt;)&lt;/code&gt;, applied to the entire capture:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkgx1rgi0geiwzwbib83w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkgx1rgi0geiwzwbib83w.png" alt="Wireshark display filter for any ICMP or any ARP originating from the DHCP server, showing zero packets displayed" width="799" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The client caught it. It ARP-probed the address it had just been given, the squatter answered, and the client sent DHCPDECLINE 3.1 seconds after the ACK — then fell back to APIPA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Autoconfiguration IPv4 Address. . : 169.254.248.228(Preferred)
Subnet Mask . . . . . . . . . . . : 255.255.0.0
Default Gateway . . . . . . . . . :
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8ecqbyngi8djglirxx7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8ecqbyngi8djglirxx7.png" alt="ipconfig output on the client showing an APIPA address and no default gateway" width="693" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the important part.&lt;/strong&gt; The client-side safety net works, so you usually do not get a duplicate IP. You get something that is arguably worse to diagnose: a machine with no network at all, sitting next to a healthy DHCP server that has just told it everything is fine.&lt;/p&gt;

&lt;p&gt;You get an actual duplicate address in two situations. The first is when the client does not probe at all: embedded devices, IoT, older or minimal stacks.&lt;/p&gt;

&lt;p&gt;The second is more common and more interesting: &lt;strong&gt;the probe happens and nobody answers it.&lt;/strong&gt; Detection — on either side — only works if the occupant is awake and willing to reply at the exact moment it is asked. A host that is powered off, asleep, on a link that is momentarily down, or sitting behind a fabric that suppresses ARP will be scored as absent. The address is handed out, and the duplicate appears later, when that host comes back and starts answering for an address someone else is now using.&lt;/p&gt;

&lt;p&gt;Note that this defeats both defences equally. The client's ARP probe and the server's ICMP ping are the same bet: that silence means free. Silence does not mean free. It means nobody replied.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the decline costs you
&lt;/h3&gt;

&lt;p&gt;The declined address goes into the lease table as &lt;code&gt;BAD_ADDRESS&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client IP Address&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Lease Expiration&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;172.16.0.10&lt;/td&gt;
&lt;td&gt;BAD_ADDRESS&lt;/td&gt;
&lt;td&gt;9/5/2026 5:39:26 PM&lt;/td&gt;
&lt;td&gt;DHCP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg970c5fhvxaqtst4p0va.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg970c5fhvxaqtst4p0va.png" alt="DHCP console showing the BAD_ADDRESS entry for 172.16.0.10 alongside the scope's 8-day lease duration" width="799" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things worth noticing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The entry expires with the scope's lease duration&lt;/strong&gt; — 8 days configured, 8 days until expiry. It is not permanent. But if the squatter is still there when it expires, the address goes back in the pool, gets offered again, gets declined again, and is marked again. It is not a leak; it is a recurring tax. Each cycle costs one unlucky client a failed boot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;Unique ID&lt;/code&gt; is &lt;code&gt;0a0010ac&lt;/code&gt;.&lt;/strong&gt; That is not a MAC address — it is &lt;code&gt;172.16.0.10&lt;/code&gt; written as little-endian hex. The entry is a mark on the address, not a record of a client. The server does not store who claimed the conflict or who reported it. There is nothing on the server side to trace the conflict back to its source: you have to go to the switch or capture packets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test 2 — &lt;code&gt;Conflict detection attempts = 2&lt;/code&gt;, same one-address scope
&lt;/h2&gt;

&lt;p&gt;Same lab, one setting changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Set-DhcpServerv4Setting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ConflictDetectionAttempts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;t (s)&lt;/th&gt;
&lt;th&gt;Who&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.000000&lt;/td&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;DISCOVER&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0.000481&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;ICMP echo request → &lt;code&gt;172.16.0.10&lt;/code&gt;&lt;/strong&gt;, TTL 255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.000812&lt;/td&gt;
&lt;td&gt;Squatter&lt;/td&gt;
&lt;td&gt;ICMP echo reply — address is alive&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpcxzdr3kyyjb817u7312.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpcxzdr3kyyjb817u7312.png" alt="Wireshark showing the DISCOVER followed by an ICMP echo request from the DHCP server to 172.16.0.10 and the reply, with no OFFER" width="800" height="120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The server probed 481 microseconds after the DISCOVER, found the address occupied, and refused to offer it. With only one address in the scope, it then had nothing to give: nine DISCOVERs across the capture, zero OFFERs.&lt;/p&gt;

&lt;p&gt;Correct behaviour, but not useful to a client. Which is why the scope needs a second address.&lt;/p&gt;

&lt;p&gt;Two details worth keeping:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Despite &lt;code&gt;attempts = 2&lt;/code&gt;, the server sent &lt;strong&gt;one&lt;/strong&gt; ping. The reply came back in 331 µs, so a second attempt was unnecessary. The setting is a maximum, not a fixed count.&lt;/li&gt;
&lt;li&gt;The probe leaves a recognisable fingerprint: &lt;strong&gt;ICMP echo request, TTL 255, sourced from the DHCP server's own address&lt;/strong&gt;. If you are staring at a capture, that is how you spot Windows conflict detection.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Test 3 — &lt;code&gt;Conflict detection attempts = 2&lt;/code&gt;, two addresses
&lt;/h2&gt;

&lt;p&gt;Scope range widened to &lt;code&gt;172.16.0.10&lt;/code&gt; – &lt;code&gt;172.16.0.11&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;t (s)&lt;/th&gt;
&lt;th&gt;Who&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.000000&lt;/td&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;DISCOVER&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.000531&lt;/td&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;td&gt;ICMP echo request → &lt;code&gt;172.16.0.10&lt;/code&gt;, TTL 255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.000846&lt;/td&gt;
&lt;td&gt;Squatter&lt;/td&gt;
&lt;td&gt;ICMP echo reply — occupied, skip it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.001517&lt;/td&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;td&gt;ARP: &lt;em&gt;who has &lt;code&gt;172.16.0.11&lt;/code&gt;?&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.586799&lt;/td&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;td&gt;ARP: &lt;em&gt;who has &lt;code&gt;172.16.0.11&lt;/code&gt;?&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.593732&lt;/td&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;td&gt;ARP: &lt;em&gt;who has &lt;code&gt;172.16.0.11&lt;/code&gt;?&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1.594280&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;OFFER — &lt;code&gt;172.16.0.11&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.595267&lt;/td&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;REQUEST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.595795&lt;/td&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;td&gt;ACK&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1uj8nvkyp6gj1o4mxhdv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1uj8nvkyp6gj1o4mxhdv.png" alt="Wireshark showing the ICMP probe to 172.16.0.10, three unanswered ARP requests for 172.16.0.11 spanning 1.59 seconds, then the OFFER of 172.16.0.11" width="800" height="173"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The client came up on &lt;code&gt;172.16.0.11&lt;/code&gt; with no DECLINE and no APIPA. It still ran its own ARP probes afterwards — the client-side check happens regardless — and then announced the address normally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the time actually goes
&lt;/h3&gt;

&lt;p&gt;This is the part I did not expect, and it is the most interesting thing in the whole exercise.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;172.16.0.10&lt;/code&gt; the server sent &lt;strong&gt;ICMP&lt;/strong&gt;. For &lt;code&gt;172.16.0.11&lt;/code&gt; it sent &lt;strong&gt;three ARP requests and no ICMP at all&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The reason is mundane and the consequence is not: the server already had an ARP cache entry for &lt;code&gt;.10&lt;/code&gt;, so it could ping it immediately. It had no entry for &lt;code&gt;.11&lt;/code&gt;, so it had to resolve the MAC first — and since nothing owns &lt;code&gt;.11&lt;/code&gt;, the ARP never resolved. The ping was never sent. The server retried ARP three times, gave up after ~1.59 seconds, concluded the address was free, and offered it.&lt;/p&gt;

&lt;p&gt;So "conflict detection by ping" only performs a ping when the address is &lt;strong&gt;occupied&lt;/strong&gt;. For a genuinely free address — which is the overwhelmingly common case in production — it degenerates into an ARP resolution that has to time out. &lt;strong&gt;The cost is not in detecting conflicts. It is in proving their absence, on every single lease.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The trade-off, measured
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Detection = 0 (default)&lt;/th&gt;
&lt;th&gt;Detection = 2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Server probes before offering&lt;/td&gt;
&lt;td&gt;No — 0 ICMP, 0 ARP from server&lt;/td&gt;
&lt;td&gt;Yes — ICMP at 481 µs, TTL 255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Occupied address offered&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who detects the conflict&lt;/td&gt;
&lt;td&gt;The client, via ARP probe&lt;/td&gt;
&lt;td&gt;The server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outcome for the client&lt;/td&gt;
&lt;td&gt;DHCPDECLINE → APIPA &lt;code&gt;169.254.x.x&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Valid address, first try&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effect on the pool&lt;/td&gt;
&lt;td&gt;Address marked &lt;code&gt;BAD_ADDRESS&lt;/code&gt; for the lease duration&lt;/td&gt;
&lt;td&gt;Address skipped, pool intact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DISCOVER → OFFER&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;406 µs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.594 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is roughly &lt;strong&gt;3,900× slower per lease&lt;/strong&gt;. On a campus network handing out thousands of leases at 8 a.m., that is not a rounding error — and it is paid on every lease, including the overwhelming majority that would never have had a conflict.&lt;/p&gt;

&lt;p&gt;This is not a Microsoft quirk. ISC, which implements the same check as &lt;code&gt;ping-check&lt;/code&gt; in &lt;code&gt;dhcpd&lt;/code&gt;, &lt;a href="https://kb.isc.org/docs/ping-check" rel="noopener noreferrer"&gt;publicly questions its value&lt;/a&gt; for exactly the same reason: it does not scale to environments that need thousands of leases per second. Two implementations, same trade-off, and neither vendor thinks the safe option is free.&lt;/p&gt;




&lt;h2&gt;
  
  
  The safety net that is not there
&lt;/h2&gt;

&lt;p&gt;The usual answer to "what about rogue DHCP servers?" is DHCP server authorization in Active Directory. It is worth being precise about what that actually does.&lt;/p&gt;

&lt;p&gt;Authorization works because the &lt;strong&gt;Microsoft&lt;/strong&gt; DHCP service refuses to start when it is not authorized in AD. A home router, a misconfigured access point, a VM running &lt;code&gt;dnsmasq&lt;/code&gt;, or any appliance with DHCP enabled asks nobody for permission. AD authorization does not see them and cannot stop them.&lt;/p&gt;

&lt;p&gt;So in the exact scenario where you need protection, both defences are absent at once: authorization does not apply to the rogue, and conflict detection is switched off.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set conflict detection to 1 or 2&lt;/strong&gt; if your scopes overlap with static hosts, or if you cannot rule out rogue servers. Measure the added latency against your lease rate before rolling it out broadly — it is a real cost, not a free win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not treat conflict detection as a fix.&lt;/strong&gt; It is a probe, and a probe only detects hosts that are awake and answering. A machine that is powered off when the lease is granted is invisible to it, and the duplicate simply shows up when that machine wakes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix it at layer 2.&lt;/strong&gt; DHCP snooping on your switches stops the rogue server instead of guessing at its consequences. That is the actual solution; conflict detection is damage control after the fact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch for &lt;code&gt;BAD_ADDRESS&lt;/code&gt; entries.&lt;/strong&gt; They are the only evidence the server keeps that something is wrong, they expire quietly with the lease duration, and they say nothing about the culprit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When a client shows up on &lt;code&gt;169.254.x.x&lt;/code&gt; with a healthy DHCP server&lt;/strong&gt;, check the lease table for &lt;code&gt;BAD_ADDRESS&lt;/code&gt; before you blame the NIC.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Method
&lt;/h2&gt;

&lt;p&gt;Lab built in VMware Workstation on a Windows host, using a LAN Segment — a virtual switch with no uplink and no host interface, fully isolated from the physical network. Server: Windows Server 2025, 24H2, build 26100.33296, DHCP role, single NIC on the lab segment. Client: Windows 10 (&lt;code&gt;WIN10-01&lt;/code&gt;). Squatter: Linux with a static address and no DHCP client running.&lt;/p&gt;

&lt;p&gt;Scope ranges were deliberately narrowed to one address (Tests 1 and 2) and two addresses (Test 3) so that address selection was deterministic. This is why the server runs out of addresses in Test 2, and it is a property of the lab, not of Windows.&lt;/p&gt;

&lt;p&gt;Captures were taken unfiltered on both the server and the client side and correlated by DHCP transaction ID; timings quoted are from the server-side capture. Before each test the &lt;code&gt;BAD_ADDRESS&lt;/code&gt; entry was deleted and the DHCP service restarted, so every run starts from an empty lease table.&lt;/p&gt;

</description>
      <category>networking</category>
      <category>windows</category>
      <category>sysadmin</category>
      <category>wireshark</category>
    </item>
  </channel>
</rss>
