[Dnsmasq-discuss] [PATCH] DHCPv6: unicast Request/Renew/Release/Decline dropped silently instead of answered with UseMulticast
Qingyang Zhou
qingyang.zhou at uwaterloo.ca
Sun Aug 9 17:09:53 UTC 2026
Hi,
dnsmasq drops every non-relay DHCPv6 message that did not arrive by multicast,
with no response at all. That is correct for four of the eight client message
types and, I think, wrong for the other four.
Present in current master: 1f12252 ("Make configuration for DHCPv4 options and
DHCPv6 options consistent.", 2026-07-27), i.e. v2.93-16-g1f12252.
RFC 8415 handles unicast client messages in two places.
Section 16 requires silence for four named types:
A server MUST discard any Solicit, Confirm, Rebind, or
Information-request messages it receives with a Layer 3 unicast
destination address.
Section 18.4 requires a reply for the others:
When the server receives a message via unicast from a client to which
the server has not sent a Server Unicast option (or is not currently
configured to do so), the server discards that message and responds
with an Advertise (when responding to a Solicit message) or Reply
message (when responding to any other messages) containing a Status
Code option (see Section 21.13) with the value UseMulticast, a Server
Identifier option (see Section 21.3) containing the server's DUID, the
Client Identifier option (see Section 21.2) from the client message (if
any), and no other options.
So Request, Renew, Release and Decline arriving by unicast should get a Reply
carrying UseMulticast, which is how the client learns to switch to multicast.
At the moment they get nothing, and the client just stalls.
The check in src/rfc3315.c applies to all eight:
/* request from a client must be multicast RFC-9915 section 16 */
if (msg_type != DHCP6RELAYFORW && !multicast_dest)
return 0;
(Incidentally "RFC-9915" there looks like a typo for 8415.)
And the status code the reply needs is defined but never used:
$ grep -rn DHCP6USEMULTI src/*.c src/*.h
src/dhcp6-protocol.h:77:#define DHCP6USEMULTI 5
Zero occurrences in any .c file, so no configuration produces the reply.
What I measured, driving dhcp6_reply() directly with multicast_dest set either
way and otherwise identical inputs:
unicast, section 18.4 group -- should get a UseMulticast Reply:
Request no response
Renew no response
Release no response
Decline no response
unicast, section 16 group -- should be silent, and are (controls):
Solicit, Confirm, Rebind, Information-request no response
multicast, the SAME four packets from the first group:
Request answered, 114 bytes
Renew answered, 90 bytes
Release answered, 120 bytes
Decline answered, 111 bytes
That last group is the one that makes the first meaningful: identical bytes,
identical server, only the arrival mode differs, and all four are answered
normally. So the silence is caused by arrival mode alone rather than by the
packets being malformed. And the middle group is there to be clear that I am
not asking for a reply to all eight -- four of them must stay silent, and
dnsmasq is right about those.
A patch is below and attached. Stated in words, in case the diff does not
survive the mail: the single early return in dhcp6_reply() becomes a block that
still returns 0 for the four message types para 16 names, and for anything else
builds the para 18.4 reply in place -- transaction-id copied from the request,
message type set to REPLY, the client's DUID echoed back if the request carried
one, the server's DUID, and a Status Code option of DHCP6USEMULTI -- then
returns DHCPV6_CLIENT_PORT. Nothing outside that block changes, and
DHCP6USEMULTI is already defined in dhcp6-protocol.h.
The Relay-forward case is deliberately left alone: a relay agent legitimately
unicasts to the server, so para 18.4 must not fire for it. The existing
`msg_type != DHCP6RELAYFORW` guard already covers that.
I built the reply inside dhcp6_reply() rather than in dhcp6_no_relay() because
the client DUID is not parsed yet at the point of the current check, and this
keeps the change to one function. If you would rather it lived elsewhere, say
so and I will redo it.
Verified with the twelve cases above: the four para 18.4 messages now get a
Reply carrying exactly Client ID, Server ID and Status Code 5 and nothing else;
the four para 16 messages stay silent; and all four still behave as before when
they arrive by multicast.
I also have a standalone reproducer: dnsmasq's own sources plus a short driver
that calls dhcp6_reply() directly, no root or network needed, running all
twelve cases above. Happy to post it.
Found with a conformance checker that builds a reference model from RFC 8415
and compares the response against the class the RFC requires. This one is
invisible to fuzzing and to differential testing, since the defect is an
absence -- no output to compare, no crash, and the drop is an early `return 0`
that any fuzzer hits constantly.
Best regards,
Zhou Qingyang
--- a/src/rfc3315.c
+++ b/src/rfc3315.c
@@ -83,9 +83,52 @@
msg_type = *((unsigned char *)daemon->dhcp_packet.iov_base);
- /* request from a client must be multicast RFC-9915 section 16 */
+ /* RFC 8415 para 16: a Solicit, Confirm, Rebind or Information-request which
+ arrives with a unicast destination address MUST be discarded silently.
+ Other client messages are handled below, per para 18.4. A Relay-forward is
+ not covered: a relay agent legitimately unicasts to the server. */
if (msg_type != DHCP6RELAYFORW && !multicast_dest)
- return 0;
+ {
+ void *opts, *opts_end, *cid;
+ int o;
+
+ if (msg_type == DHCP6SOLICIT || msg_type == DHCP6CONFIRM ||
+ msg_type == DHCP6REBIND || msg_type == DHCP6IREQ)
+ return 0;
+
+ /* RFC 8415 para 18.4: any other message received via unicast from a
+ client which was not sent a Server Unicast option is discarded, and
+ answered with a Reply carrying a Status Code of UseMulticast, the
+ server's DUID, the client's DUID if the message had one, and nothing
+ else. We never send OPTION6_UNICAST, so this always applies. */
+ opts = (unsigned char *)daemon->dhcp_packet.iov_base + 4;
+ opts_end = (unsigned char *)daemon->dhcp_packet.iov_base + sz;
+
+ reset_counter();
+
+ /* message type, then the transaction-id copied from the request */
+ if (!put_opt6(daemon->dhcp_packet.iov_base, 4))
+ return 0;
+ *((unsigned char *)daemon->outpacket.iov_base) = DHCP6REPLY;
+
+ if ((cid = opt6_find(opts, opts_end, OPTION6_CLIENT_ID, 1)))
+ {
+ o = new_opt6(OPTION6_CLIENT_ID);
+ put_opt6(opt6_ptr(cid, 0), opt6_len(cid));
+ end_opt6(o);
+ }
+
+ o = new_opt6(OPTION6_SERVER_ID);
+ put_opt6(daemon->duid, daemon->duid_len);
+ end_opt6(o);
+
+ o = new_opt6(OPTION6_STATUS_CODE);
+ put_opt6_short(DHCP6USEMULTI);
+ put_opt6_string(_("use multicast"));
+ end_opt6(o);
+
+ return DHCPV6_CLIENT_PORT;
+ }
More information about the Dnsmasq-discuss
mailing list