AI-assisted binary patching to fix an abandoned router's DHCP bug — Guru Labs Blog
An 8-byte, AI-assisted binary patch that stops EdgeOS dhcrelay3 from re-relaying already-relayed DHCP packets, an RFC 2131 violation.
~/blog $<br>cat edgeos-dhcrelay-binary-patch.md<br>Dax Kelson<br>·June 29, 2026<br>12 min read
A centralized DHCP server on an ISP network I was working on was logging about 200 duplicate request packets a second. The cause was EdgeOS's relay daemon, dhcrelay3, re-relaying packets that had already been relayed once, a violation of RFC 2131. The network runs more than 45 EdgeOS routers and the relay paths are several levels deep, which is what turned a single client request into a steady flood at the center. I fixed it with an 8-byte patch to the shipped dhcrelay3 binary. After it was deployed, the duplicates stopped: the server went from roughly 200 a second to zero.
TL;DR: one wrong branch in do_relay4() re-relays already-relayed packets. Overwrite eight bytes so it jumps to the function's existing return instead. On the Octeon (big-endian) routers:
$ cp /usr/sbin/dhcrelay3 dhcrelay3.patched<br>$ printf '\x14\xc0\xff\x56\x8f\xbf\x00\x94' | \<br>dd of=dhcrelay3.patched bs=1 seek=$((0xCF38)) count=8 conv=notrunc
Everything below is how those eight bytes were found.
Why DHCP needs a relay, and how it works
A device that does not yet have an IP address asks for one by broadcasting a DHCP DISCOVER. Broadcasts stay on the local subnet: they travel the local datalink and a router does not forward them. On a small network the DHCP server sits on that same subnet and answers directly. Large networks commonly centralize DHCP on a single server for easier management, often on its own subnet, so addressing is administered in one place rather than configured on every router. That server is not on the client's subnet, so the broadcast never reaches it by itself.
A DHCP relay bridges that gap. It runs on the local router, listens for the broadcast, and forwards the request to the configured server as an ordinary unicast packet. Before forwarding, it fills in the request's giaddr field, the "gateway IP address", with the address of the interface that received the request. That one field does two jobs: it tells the server which subnet the client is on, so the server leases an address from the right pool, and it gives the server somewhere to send the reply. The reply comes back to the relay, and the relay hands it to the client.
%%{init: {'theme':'base','fontFamily':'Open Sans, sans-serif','themeVariables':{'fontFamily':'Open Sans, sans-serif','fontSize':'16px','lineColor':'#00609A','edgeLabelBackground':'#FFFFFF','background':'#FFFFFF'},'flowchart':{'htmlLabels':true,'useMaxWidth':true,'nodeSpacing':55,'rankSpacing':70,'curve':'basis'}}}%%<br>flowchart LR<br>C["clientneeds a lease, has no IP yet"]<br>R["DHCP relayruns on the local router"]<br>S["centralDHCP server"]<br>C -->|"1. broadcast DISCOVER,stays on the local subnet"| R<br>R -->|"2. unicast to the server,giaddr = the local subnet"| S<br>classDef infra fill:#EAF2FB,stroke:#0078C1,stroke-width:2px,color:#003C60<br>classDef accent fill:#FBE3B3,stroke:#ED8C0C,stroke-width:2.5px,color:#5A3A00<br>class C,S infra<br>class R accent<br>linkStyle default stroke:#00609A,color:#1F2937
Broadcasts (step 1) never cross the router, so the relay (amber) is the only thing that carries the request to a central server (step 2). The giaddr it stamps tells the server which subnet to lease from and where to send the reply.
Once a relay has stamped giaddr, the packet is marked as already handled. RFC 2131 section 4.1.1 is explicit about what every other relay should then do with it: nothing. A request that already carries a giaddr has been relayed by another agent, so the right move is to leave it alone and let normal routing carry it to the server. EdgeOS's build only left a packet alone when its giaddr matched one of the relay's own addresses. Every other already-relayed packet it relayed a second time, which is where the loop starts.
How one DISCOVER becomes a flood
The loop needs at least two relays in the path. The first relay, R1, does the right thing: it stamps giaddr and forwards the request once toward the server. The trouble starts at the next relay the packet transits. R2 should simply route that already-stamped packet onward, but instead it re-relays it, sending a second copy toward the server with hops bumped by one. Now two packets are in flight, and each transits the relays further up the path, every one of which re-relays every copy it sees. So the copies multiply at each affected hop instead of merely adding up: a copy made by R2 gets re-relayed by R3 and R4, and the copies R3 makes are themselves re-relayed again upstream. The only brake is the BOOTP hops field, which each re-relay increments; a packet is dropped once it reaches the cap of 16. That cap bounds how deep a re-relay chain can run, not how wide the fan-out gets, so one client DISCOVER can reach the server as many more than sixteen...