Skip to content

dns: fix QU response source port and IPv6 hop limits - #35

Open
massimomazzariol wants to merge 2 commits into
openwrt:masterfrom
massimomazzariol:fix-qu-response-socket
Open

dns: fix QU response source port and IPv6 hop limits#35
massimomazzariol wants to merge 2 commits into
openwrt:masterfrom
massimomazzariol:fix-qu-response-socket

Conversation

@massimomazzariol

Copy link
Copy Markdown

Background

Hello there!
I was working on a small personal project using an old Fonera 2.0n running latest OpenWrt version, and while trying to get mDNS working reliably on it I kept running into repeated resolution problems from a Windows client.

While looking at the traffic with tcpdump, I noticed that Windows was sending an mDNS QU query for fonera.local from UDP port 5353, but the reply from umdns was originating from an ephemeral UDP source port instead of 5353.

fonera.local is just the test hostname I assigned to the device for this mDNS investigation.

The destination address and destination port looked correct, so I started following the QU response path through mdnsd to understand where the different source port was coming from.

I may be missing some design context in mdnsd, so I see this series as a proposed fix rather than the only possible solution. I would really appreciate feedback or suggestions if there is a better approach

What I found

For a question with the unicast-response bit set, parse_question() currently keeps the querier address in to, but switches from the multicast interface to the corresponding global unicast interface:

if (is_unicast) {
    to = from;
    if (interface_multicast(iface))
        iface = interface_get(iface->name,
                              iface->type | SOCKTYPE_BIT_UNICAST);
}

The global socket change was introduced here: 4035fe42

The multicast sockets are bound to UDP port 5353, while the unicast sockets are bound without setting a local port.

RFC 6762 section 6 states that the source UDP port in Multicast DNS responses must be 5353.

RFC 6762 section 5.4 also describes the unicast-response bit, which allows a querier to request a response via unicast

For additional context, 68af3114 previously changed the IPv4 unicast response path so that the destination port is taken from the querier address. The response is still sent through the selected socket, however, which is what led me to look at the source socket separately.

Based on that, I tried keeping the QU response on the mDNS socket and using the querier address only as the destination:

if (is_unicast)
    to = from;

interface_send_packet() then uses the multicast destination when no explicit destination is supplied, and the supplied destination when to is present.

With this change, the QU response is still sent directly to the querier, but originates from the socket bound to UDP port 5353.

For comparison, Apple's POSIX mDNSResponder implementation also selects the interface multicast socket separately from the destination address used for sendto():

Apple mDNSResponder - mDNSPosix.c

I do not mean this as proof that mdnsd should use the same design, but it was useful as a reference while looking at the socket behavior

Cache refresh behavior

While testing an earlier version of this change, I noticed a side effect in the cache refresh path.

cache_gc_timer() has passed the cached peer address to dns_send_question() since
480d7bc7.

Previously that destination was ignored when the interface was a multicast interface. Once an explicit destination is allowed on that socket, the same call would instead send the refresh directly to the cached peer.

To avoid changing that existing behavior, this series makes the choice explicit:

dns_send_question(r->iface,
                  interface_multicast(r->iface) ? NULL :
                  (struct sockaddr *)&r->from,
                  r->record, r->type, 0);

In my hardware test this preserved multicast-destination cache refresh queries with the QU bit set.

For unicast interfaces, the conditional continues to pass the cached peer address as before

IPv6 hop-limit issue found while testing

While testing the same change over IPv6, I found another issue that seems independent of the QU destination change.

Before applying the IPv6 socket-option fix, while testing the QU change, I observed:

  • IPv6 multicast mDNS packets leaving with hop limit 1
  • an IPv6 QU reply leaving with hop limit 64

interface_init_socket() currently has both:

uint8_t ttl = 255;
int ittl = 255;

but passes the one-byte ttl value to:

IPV6_MULTICAST_HOPS
IPV6_UNICAST_HOPS

In the Linux implementation of do_ipv6_setsockopt(), both options reject an option length smaller than sizeof(int) with EINVAL:

Linux ipv6_sockglue.c - do_ipv6_setsockopt()

The return values of these setsockopt() calls are currently not checked, so the failure is silent.

The first commit therefore only changes those two calls to use the already existing int-sized ittl value.

I kept this as a separate commit because it is a separate issue and can be reviewed independently from the QU response change

Testing

I built the resulting code using the OpenWrt 25.12.5 ramips/rt305x SDK and installed it on the Fonera 2.0n.

The tests were observed directly on the wireless interface with tcpdump

QU response

Before the QU socket change, an IPv4 QU response was observed with an ephemeral UDP source port.

With this series:

192.168.1.240.5353 > 192.168.1.81.5353
TTL 255

The same test over IPv6 produced:

fe80::218:84ff:fe88:f8f.5353
    >
fe80::176a:ddb7:c385:3990.5353

with hop limit 255.

IPv6 hop limit

Before the IPv6 socket-option fix:

IPv6 QU response:       hop limit 64
IPv6 multicast packet: hop limit 1

After changing the two socket-option arguments to ittl:

IPv6 QU response:       hop limit 255
IPv6 multicast packet: hop limit 255

A normal IPv6 QM query for fonera.local also produced a multicast response to ff02::fb:5353 with source port 5353 and hop limit 255.

RFC 6762 section 11 discusses the TTL / hop-limit value of 255 for Multicast DNS packets, including responses sent via unicast

Cache refresh

I also tested the cache refresh path separately after noticing the regression in an earlier version of the patch.

The refresh remained multicast-destination and retained the QU bit.

Final code

The two-commit series is byte-for-byte identical to the patch that was built and used for the hardware tests above

Things I intentionally did not change

There is already a TODO in parse_question() concerning the RFC 6762 section 5.4 quarter-TTL rule for deciding whether a QU response should instead be multicast, but I left that behavior unchanged.

I also did not try to handle queries originating from a UDP source port other than 5353. RFC 6762 section 6.7 treats those as legacy queries with additional response requirements, and that work is already being discussed separately in PR #18

This series is therefore not intended to claim complete RFC 6762 compliance. It only addresses the specific behaviors described above

References

Thanks for taking a look. I would especially appreciate feedback on whether keeping QU replies on the mDNS socket while using a unicast destination is the right approach for mdnsd, or if there is a better way to keep source port 5353 with the current global socket design

IPV6_MULTICAST_HOPS and IPV6_UNICAST_HOPS expect an int-sized
option value on Linux. interface_init_socket() currently passes a
uint8_t value and sizeof(uint8_t), causing setsockopt() to fail with
EINVAL.

The failure is ignored, leaving IPv6 multicast packets at the default
hop limit of 1 and unicast packets at the default hop limit of 64.

Use the existing int-sized ittl value instead. This restores the
int-sized socket option used before the global socket refactor and
sets both IPv6 hop limits to 255 as intended.

Tested on OpenWrt 25.12.5:
- multicast mDNS hop limit: 1 -> 255
- unicast QU response hop limit: 64 -> 255

Signed-off-by: Massimo Mazzariol <[email protected]>
A QU question received on the multicast socket requests a response
directly to the querier. The response is still an mDNS response and
must originate from UDP port 5353.

Since the global socket refactor in 4035fe4, parse_question() switches
QU replies to the global unicast socket. That socket is not bound to
port 5353, so the reply can use an ephemeral source port even though
the destination address and port are correct.

Keep QU replies on the mDNS socket and use the querier address only as
the destination. Allow multicast interfaces to honor an explicit
destination while continuing to use the socket bound to port 5353.

This also makes an existing destination argument in the cache refresh
path significant. Preserve its historical behavior explicitly:
refreshes on multicast interfaces remain multicast-destination QU
queries, while unicast interfaces continue to query the cached peer.

Hardware validation on OpenWrt 25.12.5 confirmed:
- IPv4 QU replies use UDP 5353 -> 5353 with TTL 255
- IPv6 QU replies use UDP 5353 -> 5353 with hop limit 255
- normal multicast responses remain multicast
- multicast cache refresh queries remain multicast with the QU bit

Signed-off-by: Massimo Mazzariol <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant