diff --git a/src/resolve/resolved-dns-scope.c b/src/resolve/resolved-dns-scope.c index 6a20fa20da..7e19f12469 100644 --- a/src/resolve/resolved-dns-scope.c +++ b/src/resolve/resolved-dns-scope.c @@ -1533,10 +1533,57 @@ static int on_announcement_timeout(sd_event_source *s, usec_t usec, void *userda return 0; } -int dns_scope_send_goodbye(DnsScope *scope, DnsAnswer *answer) { - _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; - int r; +static int dns_scope_emit_announcement(DnsScope *scope, DnsAnswer *answer) { + size_t max_size, fragmented_max; + unsigned n_sent = 0, n_records = 0; + int r, ret = 0; + assert(scope); + assert(scope->protocol == DNS_PROTOCOL_MDNS); + + /* Once the loop is finished the mDNS sockets are gone, and opening one would register I/O + * on it. Guarded here, in the one emitter every announcement and withdrawal goes through. */ + r = sd_event_get_state(scope->manager->event); + if (r < 0) + return log_debug_errno(r, "Failed to get event loop state: %m"); + if (r == SD_EVENT_FINISHED) + return 0; + + /* An mDNS scope always belongs to a link; its MTU may still be unknown (0) this early. */ + assert(scope->link); + mdns_announcement_max_sizes(scope->family, scope->link->mtu, &max_size, &fragmented_max); + + DnsPacket **packets = NULL; + size_t n_packets = 0; + CLEANUP_ARRAY(packets, n_packets, dns_packet_unref_array); + + r = mdns_announcement_packetize(answer, max_size, fragmented_max, &packets, &n_packets); + if (r < 0) + return r; + if (r > 0) + /* Warn, not debug: a withdrawal's caller drops these from the zone regardless, so a + * record skipped here is never withdrawn and peers keep it for its full TTL. */ + log_warning("Cannot fit %i mDNS record(s) into any packet on scope %s, not sending them.", + r, dns_scope_ifname(scope) ?: "*"); + + /* Best effort per packet, so one failed send does not withhold the rest. Each packet also + * takes its own token from the scope's multicast rate limit, which bounds datagrams. */ + FOREACH_ARRAY(p, packets, n_packets) { + r = dns_scope_emit_udp(scope, /* fd= */ -EBADF, AF_UNSPEC, *p); + RET_GATHER(ret, r); + if (r >= 0) { + n_sent++; + n_records += DNS_PACKET_ANCOUNT(*p); + } + } + + log_debug("Emitted %u mDNS announcement packet(s) carrying %u record(s) on scope %s.", + n_sent, n_records, dns_scope_ifname(scope) ?: "*"); + + return ret; +} + +int dns_scope_send_goodbye(DnsScope *scope, DnsAnswer *answer) { assert(scope); assert(answer); @@ -1546,32 +1593,14 @@ int dns_scope_send_goodbye(DnsScope *scope, DnsAnswer *answer) { if (scope->protocol != DNS_PROTOCOL_MDNS) return 0; - r = sd_event_get_state(scope->manager->event); - if (r < 0) - return log_debug_errno(r, "Failed to get event loop state: %m"); - - /* If this is called on exit, through manager_free() -> link_free(), then we cannot announce. */ - if (r == SD_EVENT_FINISHED) - return 0; - if (dns_answer_isempty(answer)) return 0; - r = dns_scope_make_reply_packet(scope, /* id= */ 0, DNS_RCODE_SUCCESS, /* q= */ NULL, answer, - /* soa= */ NULL, /* tentative= */ false, &p); - if (r < 0) - return log_debug_errno(r, "Failed to build reply packet: %m"); - - r = dns_scope_emit_udp(scope, -EBADF, AF_UNSPEC, p); - if (r < 0) - return log_debug_errno(r, "Failed to send reply packet: %m"); - - return 0; + return dns_scope_emit_announcement(scope, answer); } int dns_scope_announce(DnsScope *scope, bool goodbye) { _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; - _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; _cleanup_set_free_ Set *types = NULL; DnsZoneItem *z; unsigned size = 0; @@ -1679,13 +1708,14 @@ int dns_scope_announce(DnsScope *scope, bool goodbye) { if (dns_answer_isempty(answer)) return 0; - r = dns_scope_make_reply_packet(scope, 0, DNS_RCODE_SUCCESS, NULL, answer, NULL, false, &p); + /* Emission is best effort per packet, so a single failed send (a rate limit, a transient + * -ENOBUFS) does not cost the announcement its second transmission below. That repetition + * is scheduled by a pass that finds the scope unannounced -- its first, and any after the + * scope's records were re-added -- and is the only retry a lost packet gets: a send that + * fails on a pass that schedules nothing is logged and not repeated. */ + r = dns_scope_emit_announcement(scope, answer); if (r < 0) - return log_debug_errno(r, "Failed to build reply packet: %m"); - - r = dns_scope_emit_udp(scope, -1, AF_UNSPEC, p); - if (r < 0) - return log_debug_errno(r, "Failed to send reply packet: %m"); + log_debug_errno(r, "Failed to emit some announcement packets, ignoring: %m"); /* In section 8.3 of RFC6762: "The Multicast DNS responder MUST send at least two unsolicited * responses, one second apart." */ diff --git a/src/resolve/resolved-mdns.c b/src/resolve/resolved-mdns.c index fb20ba9cd0..74c81fd736 100644 --- a/src/resolve/resolved-mdns.c +++ b/src/resolve/resolved-mdns.c @@ -1,6 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include +#include #include "sd-event.h" @@ -699,3 +701,206 @@ int manager_mdns_ipv6_fd(Manager *m) { return m->mdns_ipv6_fd = TAKE_FD(s); } + +static int mdns_announcement_packet_new(size_t max_size, DnsPacket **ret) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + int r; + + assert(ret); + + r = dns_packet_new(&p, DNS_PROTOCOL_MDNS, /* min_alloc_dsize= */ 0, max_size); + if (r < 0) + return r; + + DNS_PACKET_HEADER(p)->flags = htobe16(DNS_PACKET_MAKE_FLAGS( + 1 /* qr */, + 0 /* opcode */, + 1 /* aa, see RFC 6762, section 18.4 */, + 0 /* tc */, + 0 /* (tentative) */, + 0 /* (ra) */, + 0 /* (ad) */, + 0 /* (cd) */, + DNS_RCODE_SUCCESS)); + + *ret = TAKE_PTR(p); + return 0; +} + +/* Append one record to the packet under construction, creating it first if there is none, and + * count it in the packet's own header. Returns -EMSGSIZE when the record does not fit, which the + * caller answers by sealing what it has (or by widening the packet), exactly as it would for a bare + * dns_packet_append_rr(). */ +static int mdns_announcement_packet_append(DnsPacket **p, size_t max_size, DnsAnswerItem *item) { + int r; + + assert(p); + assert(item); + + if (!*p) { + r = mdns_announcement_packet_new(max_size, p); + if (r < 0) + return r; + } + + r = dns_packet_append_rr(*p, item->rr, item->flags, + /* start= */ NULL, /* rdata_start= */ NULL); + if (r < 0) + return r; + + DNS_PACKET_HEADER(*p)->ancount = htobe16(DNS_PACKET_ANCOUNT(*p) + 1); + return 0; +} + +/* Push the announcement packet under construction onto the result array; its ancount has been kept + * current by every append. A no-op without a packet or without any RRs appended yet. */ +static int mdns_announcement_packet_seal( + DnsPacket ***packets, + size_t *n_packets, + DnsPacket **p) { + + assert(packets); + assert(n_packets); + assert(p); + + if (!*p || DNS_PACKET_ANCOUNT(*p) == 0) + return 0; + + if (!GREEDY_REALLOC(*packets, *n_packets + 1)) + return -ENOMEM; + + (*packets)[(*n_packets)++] = TAKE_PTR(*p); + + return 0; +} + +/* The datagram size every host of the family must be able to accept: RFC 791 section 3.1 for IPv4, + * RFC 8200 section 5's minimum link MTU for IPv6, which no link fragments. */ +static size_t fallback_mtu(int family) { + assert(IN_SET(family, AF_INET, AF_INET6)); + + return family == AF_INET6 ? IPV6_MIN_MTU : MDNS_PACKET_UNKNOWN_MTU_IPV4; +} + +/* Derive the two size bounds an announcement is packed to, from the address family and the link MTU + * (0 while the link's MTU is not known yet, or too small to carry a packet). + * + * RFC 6762 section 17's hard ceiling -- "even when fragmentation is used, a Multicast DNS packet, + * including IP and UDP headers, MUST NOT exceed 9000 bytes" -- binds every packet we emit, so it caps + * the MTU-derived size on jumbo-frame links just as much as it caps the lone oversized-RR fallback. + * A fragmented IPv6 datagram additionally carries an 8-byte Fragment extension header (RFC 8200 + * section 4.5) that counts against the ceiling. */ +void mdns_announcement_max_sizes( + int family, + size_t link_mtu, + size_t *ret_max_size, + size_t *ret_fragmented_max) { + + size_t header_size, fragmented_max, max_size; + + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(ret_max_size); + assert(ret_fragmented_max); + + header_size = udp_header_size(family); + assert(MDNS_PACKET_FRAGMENTED_SIZE_MAX > + header_size + sizeof(struct ip6_frag) + DNS_PACKET_HEADER_SIZE); + fragmented_max = MDNS_PACKET_FRAGMENTED_SIZE_MAX - header_size - + (family == AF_INET6 ? sizeof(struct ip6_frag) : 0); + + /* An unsolicited DNS-SD announcement (or goodbye) covers the whole zone, which easily outgrows + * both the compression pointer range that dns_packet_append_name() can address and the interface + * MTU that RFC 6762 section 17 expects multicast DNS messages to fit into. Instead of emitting + * one oversized datagram, split the RRset across as many MTU-sized packets as needed. */ + if (link_mtu > header_size + DNS_PACKET_HEADER_SIZE) + max_size = link_mtu - header_size; + else + /* Nothing to derive from: the MTU is not read yet (0), or subtracting the headers + * would underflow or leave less than the DNS header dns_packet_new() asserts room + * for. Assume what every host of the family must accept instead -- not the section + * 17 ceiling, which is the bound for a datagram deliberately fragmented to carry one + * record. A derivable bound is kept even where no record fits it: the records then + * go out one per fragmented packet, which is the only shape section 17 allows. */ + max_size = fallback_mtu(family) - header_size; + + *ret_max_size = MIN(max_size, fragmented_max); + *ret_fragmented_max = fragmented_max; +} + +/* Pack the answer's records into as many announcement packets of at most max_size bytes as needed, + * in answer order, each with its ancount finalized. A single record that does not fit any packet + * on its own is retried alone within fragmented_max -- RFC 6762 section 17 allows a lone resource + * record to rely on IP fragmentation, up to a hard 9000-byte ceiling including headers -- and + * dropped if even that bound cannot accommodate it, as is a record that cannot be encoded at any + * size. Returns how many were dropped that way, so a caller can report it: nothing else + * distinguishes an announcement that carried everything from one that did not. */ +int mdns_announcement_packetize( + DnsAnswer *answer, + size_t max_size, + size_t fragmented_max, + DnsPacket ***ret_packets, + size_t *ret_n_packets) { + + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + DnsPacket **packets = NULL; + size_t n_packets = 0; + DnsAnswerItem *item; + unsigned n_skipped = 0; + int r; + + assert(max_size <= fragmented_max); + assert(ret_packets); + assert(ret_n_packets); + + CLEANUP_ARRAY(packets, n_packets, dns_packet_unref_array); + + DNS_ANSWER_FOREACH_ITEM(item, answer) { + r = mdns_announcement_packet_append(&p, max_size, item); + if (r == -EMSGSIZE && p && DNS_PACKET_ANCOUNT(p) > 0) { + /* Packet full -- seal it and retry this RR in a fresh one. */ + r = mdns_announcement_packet_seal(&packets, &n_packets, &p); + if (r < 0) + return r; + + r = mdns_announcement_packet_append(&p, max_size, item); + } + if (r == -EMSGSIZE && max_size < fragmented_max) { + /* A single RR larger than the MTU. Emit it alone in an oversized packet + * within the section 17 fragmented-packet ceiling. */ + p = dns_packet_unref(p); + + r = mdns_announcement_packet_append(&p, fragmented_max, item); + if (r >= 0) { + /* Sealed alone: nothing that follows may be packed to this bound. */ + r = mdns_announcement_packet_seal(&packets, &n_packets, &p); + if (r < 0) + return r; + + continue; + } + } + /* No packet we can build fits this record: too large even for the section 17 + * ceiling, or not encodable at all (-E2BIG; -ENOSPC is defensive, no bound we pack + * to lets a packet grow that far). Skip it rather than fail the announcement -- + * dns_packet_append_rr() rolled the packet back, so its neighbours are intact. An + * empty packet is dropped: the fragmented retry may have widened it, and the + * records that follow must not be packed to that bound. */ + if (IN_SET(r, -EMSGSIZE, -ENOSPC, -E2BIG)) { + log_debug_errno(r, "Skipping resource record that does not fit an mDNS packet: %m"); + if (p && DNS_PACKET_ANCOUNT(p) == 0) + p = dns_packet_unref(p); + n_skipped++; + continue; + } + if (r < 0) + return r; + } + + r = mdns_announcement_packet_seal(&packets, &n_packets, &p); + if (r < 0) + return r; + + *ret_n_packets = n_packets; + *ret_packets = TAKE_PTR(packets); + return (int) n_skipped; +} diff --git a/src/resolve/resolved-mdns.h b/src/resolve/resolved-mdns.h index fd9ecc944f..d68a6af46e 100644 --- a/src/resolve/resolved-mdns.h +++ b/src/resolve/resolved-mdns.h @@ -6,9 +6,31 @@ #define MDNS_PORT 5353 #define MDNS_ANNOUNCE_DELAY (1 * USEC_PER_SEC) +/* RFC 6762 section 17: "Even when fragmentation is used, a Multicast DNS packet, including IP and UDP + * headers, MUST NOT exceed 9000 bytes." */ +#define MDNS_PACKET_FRAGMENTED_SIZE_MAX 9000U + +/* What to assume while the interface MTU is not known yet: the datagram size every host of the + * family must be able to accept, RFC 791 section 3.1 -- whole or reassembled from fragments. (For + * IPv6 the corresponding bound is IPV6_MIN_MTU, RFC 8200 section 5's minimum link MTU, which is a + * stronger guarantee: no link fragments a datagram that size.) */ +#define MDNS_PACKET_UNKNOWN_MTU_IPV4 576U + int manager_mdns_ipv4_fd(Manager *m); int manager_mdns_ipv6_fd(Manager *m); void manager_mdns_stop(Manager *m); void manager_mdns_maybe_stop(Manager *m); int manager_mdns_start(Manager *m); + +void mdns_announcement_max_sizes( + int family, + size_t link_mtu, + size_t *ret_max_size, + size_t *ret_fragmented_max); +int mdns_announcement_packetize( + DnsAnswer *answer, + size_t max_size, + size_t fragmented_max, + DnsPacket ***ret_packets, + size_t *ret_n_packets); diff --git a/src/shared/dns-packet.c b/src/shared/dns-packet.c index a0508b1c68..4f7353d963 100644 --- a/src/shared/dns-packet.c +++ b/src/shared/dns-packet.c @@ -297,6 +297,8 @@ DnsPacket *dns_packet_unref(DnsPacket *p) { return NULL; } +DEFINE_POINTER_ARRAY_FREE_FUNC(DnsPacket*, dns_packet_unref); + int dns_packet_validate(DnsPacket *p) { assert(p); diff --git a/src/shared/dns-packet.h b/src/shared/dns-packet.h index 1bf577a7ce..90c5187091 100644 --- a/src/shared/dns-packet.h +++ b/src/shared/dns-packet.h @@ -160,6 +160,7 @@ void dns_packet_set_flags(DnsPacket *p, bool dnssec_checking_disabled, bool trun DnsPacket *dns_packet_ref(DnsPacket *p); DnsPacket *dns_packet_unref(DnsPacket *p); +void dns_packet_unref_array(DnsPacket **array, size_t n); DEFINE_TRIVIAL_CLEANUP_FUNC(DnsPacket*, dns_packet_unref);