[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