resolved: split oversized mDNS announcements across MTU-sized packets
dns_scope_announce() put the entire zone -- for DNS-SD every PTR/SRV/TXT record of every published service -- into a single reply packet. With a few hundred published services that grows far beyond the interface MTU, and beyond what RFC 6762 section 17 permits: a packet larger than the MTU is sent using fragments, and such a packet "MUST NOT contain more than one resource record". Pack the announcement into as many MTU-sized packets as needed instead. A single record that does not fit the MTU by itself is emitted alone in an oversized packet, relying on IP fragmentation, which the same section explicitly allows for that case -- bounded by its hard 9000-byte ceiling, headers included, which also caps jumbo-frame links. While the interface MTU is not known yet, packets are cut to the datagram size every host of the family must be able to accept -- RFC 791 section 3.1's 576 octets for IPv4, whole or reassembled, and RFC 8200 section 5's 1280-byte minimum link MTU for IPv6, which no link fragments -- rather than to that ceiling, so an announcement is not packed for deliberate fragmentation merely because the MTU was not read yet. A record that cannot be encoded at any size -- a character string past 255 bytes, say -- is skipped like one too large for the ceiling, rather than failing the whole announcement: dns_packet_append_rr() rolls the packet back on every failure, so its neighbours are unaffected either way. The packetizer returns how many it dropped and the scope warns on it, since nothing else would distinguish an announcement that carried everything from one that did not -- and for a withdrawal that matters: the caller drops the records from the zone either way, so one skipped here is never withdrawn at all and peers keep it for its full TTL. The packing itself lives in resolved-mdns.c as mdns_announcement_packetize(), taking an answer and the two size bounds and returning finalized packets, so it can be tested without a scope, a link or a socket. Emission is best effort per packet -- a failed send no longer withholds the announcement's remaining packets -- and each pass logs the packet and record counts it actually put on the wire. Each packet takes its own token from the scope's multicast rate limit: that limit bounds datagrams on the wire, and an announcement that would exhaust its burst is the very flood it exists to cap. dns_scope_send_goodbye(), the other unsolicited-response emitter, goes through the same packetizer: a withdrawal of many records had the same oversized-packet problem, and a single record failing to append abandoned the whole withdrawal. Its return value follows the emitter's now, so a withdrawal split across packets reports an error when any one of them fails to go out.
This commit is contained in:
committed by
Luca Boccassi
parent
c99af7968e
commit
d96c288ed4
@@ -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." */
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <linux/ipv6.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/ip6.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user