Handling Broken DHCPv6 Client Implementations
We recently ran into a few surprising cases of incorrect DHCPv6 client behavior across several vendor implementations (Fritz!box and Telstra Netgear V7610 CPE). These clients were failing to properly request or renew leases, leading to inconsistent connectivity and degraded user experience. This post breaks down how we diagnosed the issue, the workarounds we implemented, and what it reveals about the broader state of IPv6 support in consumer devices.
Murry Mukhtarov, March 23, 2025

Introduction
Before we dive into the issue, here are a couple of notes regarding our IPv6 implementation. Each Neptune Internet subscriber has two sets of IPv6 prefixes:
- A GUA /64 prefix that we assign to every subscriber.
- An IPv6 DHCPv6 PD /48 prefix that we assign to the subscriber so they can manage their internal IPv6 network.
Both prefixes are distributed via DHCPv6. SLAAC is enabled on the interface; however, it instructs the neighbor to obtain additional settings via DHCPv6 using the M and O flags.
- The client generates a link-local address using the fe80::/64 prefix.
- A Router Solicitation (RS) is sent to ff02::2 (all-routers multicast).
- Our BNG sends a Router Advertisement (RA) with prefix information for SLAAC (which we don’t use) and sets the M (Managed) and O (Other) flags.
- The client uses its link-local address to request a DHCPv6 lease for address assignment and additional configuration.
Our BNG routers relay DHCPv6 requests to our DHCP server, which is based on CoreDHCP. We use the CoreDHCP plugin system to communicate with our backend and resolve IPv6 (and IPv4) prefixes into NBN AVC IDs. NBN sends the AVC ID with every DHCPv4/v6 request we receive from our subscribers.
We noticed that for a very small subset of customers, CoreDHCP was throwing an error when attempting to assign an address. Specifically, the error was:
vendor class data should not be empty
Troubleshooting
General troubleshooting for IPv6 issues at Neptune Internet involves checking if the RA (Router Advertisement) and IPv6 ND (Neighbor Discovery) processes were successful.
Assuming our customer's interface is Te0/2/0.1280049
(we encode the C-tag and
S-tag into the interface name for simplicity of automation), we first check for
the IPv6 neighbor on the given interface. As you can see in the example below,
the neighbor is reachable. This led us to believe that the IPv6 stack of our
subscriber was functional and enabled on their CPE.
sh ipv6 neighbors | i 1280049
FE80::4A5D:35FF:FE91:A465 0 485d.3591.a465 REACH Te0/2/0.1280049
We were able to ping the address
ping ipv6 FE80::4A5D:35FF:FE91:A465
Output Interface: TenGigabitEthernet0/2/0.1280049
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to FE80::4A5D:35FF:FE91:A465, timeout is 2 seconds:
Packet sent with a source address of FE80::6E20:56FF:FE6C:B220%TenGigabitEthernet0/2/0.1280049
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 5/5/5 ms
However, after checking if the framed routes were installed for GUA and IPv6 PD, we noticed that there was no output.
sh ipv6 route interface tenGigabitEthernet 0/2/0.1280049
Traffic capture indicated that the customer was sending a DHCPv6 request, but our DHCP server never replied.
Pinpointing the error
The error was located in the dhcp package that CoreDHCP uses to parse DHCP requests. The library was overly strict and did not permit an empty vendor class. Fritz!Box and Netgear routers were both sending a valid vendor class option, but the data was empty.
Solution
At Neptune Internet, we believe in prioritizing our customers. Our team has patched our CoreDHCP package by padding the empty vendor class with default data that has the correct payload and length.
One may argue that we are fixing an error in the implementation and should instead insist on the correctness of DHCPv6 client behavior. However, given that multiple consumer routers and CPE vendors send empty payloads, we should accommodate this equipment to increase IPv6 adoption.
A small logic update that shouldn't have long term consequence was pushed to the dependency. We rebuilt the package with a new version of the parser and our customer successfully acquired DHCPv6 leases.
diff --git a/dhcpv6/option_vendorclass.go b/dhcpv6/option_vendorclass.go
index f85795e..9f1e329 100644
--- a/dhcpv6/option_vendorclass.go
+++ b/dhcpv6/option_vendorclass.go
@@ -49,7 +49,10 @@ func (op *OptVendorClass) FromBytes(data []byte) error {
op.Data = append(op.Data, buf.CopyN(int(len)))
}
if len(op.Data) == 0 {
- return fmt.Errorf("%w: vendor class data should not be empty", uio.ErrBufferTooShort)
+ op.Data = [][]byte{
+ []byte("DEFAULT"),
+ []byte("hh"),
+ }
}
return buf.FinError()
}
The customer now has routes that were previously missing
sh ipv6 route interface tenGigabitEthernet 0/2/0.1280049
S 2401:2520:4231:E::/128 [1/0]
via FE80::4A5D:35FF:FE91:A465, TenGigabitEthernet0/2/0.1280049
S 2401:2520:310E::/48 [1/0]
via FE80::4A5D:35FF:FE91:A465, TenGigabitEthernet0/2/0.1280049