[Dnsmasq-discuss] [PATCH] DHCPv6: Request with IA_PD answered with top-level NoAddrsAvail and no IA option
Qingyang Zhou
qingyang.zhou at uwaterloo.ca
Sun Aug 9 17:46:59 UTC 2026
Hi,
A Request carrying an IA_PD gets a Reply with a top-level NoAddrsAvail (2) and
no IA option at all. RFC 8415 section 18.3.2 asks for an IA_PD carrying
NoPrefixAvail (6):
For any IA_PD option (see Section 21.21) in the Request message to which
the server cannot assign any delegated prefixes, the server MUST include
an IA_PD option in the Reply message with a Status Code option with the
value NoPrefixAvail in it.
I realise dnsmasq does not do prefix delegation -- check_ia() rejects anything
that is not IA_NA or IA_TA, so the IA_PD is skipped and execution falls
through to the generic "no address was assigned" path. So you may well read
this as a feature request rather than a bug, which is fair.
The reason I am sending it anyway is that the current reply is not just
incomplete, it is misleading, and that part is independent of whether you ever
implement delegation.
Present in current master: 1f12252 ("Make configuration for DHCPv4 options and
DHCPv6 options consistent.", 2026-07-27), i.e. v2.93-16-g1f12252.
With --dhcp-range=2001:db8:1::2,2001:db8:1::ff,64,3600 -- a perfectly healthy
address pool -- these two Requests, differing only in which IA they carry:
IA_NA:
option 3 len 40 IA_NA
sub-option 5 len 24 IAADDR
option 13 len 9 STATUS_CODE = 0
-> an address is assigned. Addresses ARE available.
IA_PD:
option 13 len 24 STATUS_CODE = 2 (NoAddrsAvail), at the top level
-> no IA option in the reply at all
So a client that asked only for a prefix is told that no addresses are
available, on a server that is handing addresses out. Whatever the right
long-term answer about delegation, "NoAddrsAvail" is not true here.
The other thing worth noting is that dnsmasq already produces the shape
section 18.3.2 wants when it cannot satisfy an IA it does support. With
--dhcp-range=2001:db8:1::,static,64,3600, so that no dynamic address can be
served:
IA_NA:
option 3 len 37 IA_NA
sub-option 13 len 21 STATUS_CODE = 2 (NoAddrsAvail)
option 13 len 24 STATUS_CODE = 2
-> the IA is built and the status is inside it, as 18.3.2 asks
So the per-IA placement is already right for addresses; it is only the IA_PD
case that gets no IA and a top-level status about the wrong resource.
A patch is below and attached. It does the minimal thing rather than
implementing delegation: it adds DHCP6NOPREFIX (6) to dhcp6-protocol.h, and in
the DHCP6REQUEST case it answers an IA_PD it cannot serve with an IA_PD carrying
that status -- echoing the client's IAID, T1/T2 zero, status inside the IA --
instead of skipping the option. It also stops the top-level NoAddrsAvail being
emitted when no address IA was requested at all, since the refusal is then
already reported inside the IA_PD. Roughly 44 added lines across the two files.
One case it does NOT fix, which you will spot faster than I did: a Request that
mixes an IA_PD with an IA_NA or IA_TA that carries no addresses. That path does
save_counter(start);
goto request_no_address;
which rewinds the output buffer -- discarding the IA_PD I just wrote -- and
hands the message to the Solicit code. Covering it means emitting the same
refusal on that path too, and I did not want to widen the patch into the Solicit
case uninvited. Behaviour there is byte-identical to today, so nothing
regresses; it just stays unfixed. Say the word and I will extend it.
Verified with the cases above, on both configurations: an IA_PD-only Request now
gets an IA_PD carrying NoPrefixAvail, the address paths are untouched, and the
mixed case is unchanged from current behaviour.
For completeness: NoPrefixAvail has never existed in the tree -- `git log -S
NOPREFIX` returns nothing, and dhcp6-protocol.h defines status codes 0..5.
I have a standalone reproducer if useful: dnsmasq's own sources plus a short
driver calling dhcp6_reply() directly, no root or network, running all four
cases above across the two configs.
Found with a conformance checker that builds a reference model from RFC 8415
and compares the reply against the class the RFC requires. It keeps
NoAddrsAvail and NoPrefixAvail as distinct outcomes, which is what makes the
substitution visible -- an oracle that only recorded "a status code came back"
would have accepted it.
Best regards,
Zhou Qingyang
--- a/src/dhcp6-protocol.h
+++ b/src/dhcp6-protocol.h
@@ -75,3 +75,4 @@
#define DHCP6NOBINDING 3
#define DHCP6NOTONLINK 4
#define DHCP6USEMULTI 5
+#define DHCP6NOPREFIX 6
--- a/src/rfc3315.c
+++ b/src/rfc3315.c
@@ -830,6 +830,7 @@
case DHCP6REQUEST:
{
int address_assigned = 0;
+ int addr_ia_seen = 0, prefix_refused = 0;
int start = save_counter(-1);
/* set reply message type */
@@ -848,7 +849,36 @@
int t1cntr;
if (!check_ia(state, opt, &ia_end, &ia_option))
- continue;
+ {
+ /* RFC 8415 para 18.3.2: "For any IA_PD option in the Request
+ message to which the server cannot assign any delegated
+ prefixes, the server MUST include an IA_PD option in the
+ Reply message with a Status Code option with the value
+ NoPrefixAvail in it." We do not implement prefix delegation,
+ so we can never assign one -- but the refusal still has to be
+ reported in an IA_PD, not as a top-level status about
+ addresses. */
+ if (opt6_type(opt) == OPTION6_IA_PD && opt6_len(opt) >= 12)
+ {
+ int opd = new_opt6(OPTION6_IA_PD);
+ int opd1;
+
+ put_opt6_long(opt6_uint(opt, 0, 4)); /* echo the IAID */
+ put_opt6_long(0); /* T1 */
+ put_opt6_long(0); /* T2 */
+
+ opd1 = new_opt6(OPTION6_STATUS_CODE);
+ put_opt6_short(DHCP6NOPREFIX);
+ put_opt6_string(_("no prefixes available"));
+ end_opt6(opd1);
+ end_opt6(opd);
+
+ prefix_refused = 1;
+ }
+ continue;
+ }
+
+ addr_ia_seen = 1;
if (!ia_option)
{
@@ -925,6 +955,16 @@
o1 = new_opt6(OPTION6_STATUS_CODE);
put_opt6_short(DHCP6SUCCESS);
put_opt6_string(_("success"));
+ end_opt6(o1);
+ }
+ else if (!addr_ia_seen && prefix_refused)
+ {
+ /* Only prefixes were requested, and the refusal is already reported
+ inside the IA_PD above. A top-level NoAddrsAvail here would name a
+ resource the client never asked for. */
+ o1 = new_opt6(OPTION6_STATUS_CODE);
+ put_opt6_short(DHCP6SUCCESS);
+ put_opt6_string(_("success"));
end_opt6(o1);
}
else
More information about the Dnsmasq-discuss
mailing list