resolved: split oversized mDNS announcements across MTU-sized packets (#43667)

Split out of https://github.com/systemd/systemd/pull/42983

## Problem
`dns_scope_announce()` put the entire zone into a single reply packet —
for DNS-SD that is every PTR/SRV/TXT record of every published service.
With a few hundred services that grows far past the interface MTU, and
past what RFC 6762 section 17 allows: a Multicast DNS packet larger than
the MTU is sent using fragments, and such a packet "MUST NOT contain
more than one resource record".

## Fix
Pack an announcement into as many MTU-sized packets as needed. A record
that does not fit the MTU on its own is emitted alone in an oversized
packet relying on IP fragmentation — the case section 17 explicitly
allows — bounded by its hard 9000-byte ceiling including headers, which
also caps jumbo-frame links.

Without a usable interface MTU (no link on the scope, or `IFLA_MTU` not
read yet) packets are cut to the largest datagram a host of the family
must accept unfragmented — RFC 791's 576 bytes for IPv4, RFC 8200
section 5's 1280-byte minimum link MTU for IPv6 — rather than to that
ceiling, so an announcement is never fragmented merely because the MTU
was not known yet, and the lone-oversized-record path stays reachable.

A record that cannot be encoded at any size (an rdata past 64KiB, a
character string past 255 bytes) 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 packing lives in `resolved-mdns.c` as
`mdns_announcement_packetize()`, taking an answer and the two size
bounds and returning finalized packets, so it is testable without a
scope, a link or a socket. Emission is best effort per packet — a failed
send no longer withholds the rest — and each pass logs the packet and
record counts it actually put on the wire.
This commit is contained in:
Yu Watanabe
2026-09-23 11:36:09 +09:00
committed by GitHub
7 changed files with 869 additions and 28 deletions
+3
View File
@@ -120,6 +120,9 @@ executables += [
resolve_test_template + {
'sources' : files('test-dns-question.c'),
},
resolve_test_template + {
'sources' : files('test-mdns-announce.c'),
},
resolve_test_template + {
'sources' : files('test-resolved-etc-hosts.c'),
},
+58 -28
View File
@@ -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." */
+205
View File
@@ -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;
}
+22
View File
@@ -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);
+578
View File
@@ -0,0 +1,578 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <linux/ipv6.h>
#include <netinet/in.h>
#include "alloc-util.h"
#include "dns-answer.h"
#include "dns-packet.h"
#include "dns-rr.h"
#include "macro.h"
#include "resolved-mdns.h"
#include "string-util.h"
#include "strv.h"
#include "tests.h"
static DnsResourceRecord* a_rr(const char *name) {
DnsResourceRecord *rr;
rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_A, name);
ASSERT_NOT_NULL(rr);
rr->ttl = 120;
rr->a.in_addr.s_addr = htobe32(0xC0A80101);
return rr;
}
/* A TXT record with a single character string of the given size. */
static DnsResourceRecord* txt_rr(const char *name, size_t item_size) {
DnsResourceRecord *rr;
DnsTxtItem *item;
rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_TXT, name);
ASSERT_NOT_NULL(rr);
rr->ttl = 120;
item = malloc0(offsetof(DnsTxtItem, data) + item_size + 1);
ASSERT_NOT_NULL(item);
item->length = item_size;
memset(item->data, 'x', item_size);
rr->txt.items = item;
return rr;
}
/* The wire size one record takes at the start of a fresh packet. The test names below share no
* suffix (except where a testcase says so), so compression never shrinks later occurrences and
* every record costs this much. */
static size_t rr_wire_size(DnsResourceRecord *rr) {
_cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
size_t before;
ASSERT_OK(dns_packet_new(&p, DNS_PROTOCOL_MDNS, /* min_alloc_dsize= */ 0, DNS_PACKET_SIZE_MAX));
before = p->size;
ASSERT_OK(dns_packet_append_rr(p, rr, /* flags= */ 0, /* start= */ NULL, /* rdata_start= */ NULL));
return p->size - before;
}
/* Adds a record for each name and returns their common wire size, which the packing bounds in the
* cases below are expressed in multiples of -- so it also asserts that they really are equal. */
static size_t answer_add_equal_sized(DnsAnswer *answer, char * const *names) {
size_t sz = 0;
assert(answer);
STRV_FOREACH(name, names) {
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = a_rr(*name);
if (sz == 0)
sz = rr_wire_size(rr);
else
ASSERT_EQ(rr_wire_size(rr), sz);
ASSERT_OK(dns_answer_add(answer, rr, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
}
return sz;
}
/* Parses every packet back and returns the owner names in packet order, so a caller can pin that
* each record went out exactly once and in order -- a count would miss one duplicated and one lost.
* Releases the packets and the array. */
static char** check_and_free_packets(DnsPacket **packets, size_t n_packets) {
_cleanup_strv_free_ char **names = NULL;
DnsResourceRecord *rr;
FOREACH_ARRAY(p, packets, n_packets) {
/* Positive, not just non-negative: dns_packet_validate_reply() returns 0 rather than
* an error for a packet whose header says "query", so ASSERT_OK would accept one.
* QR and AA are what make this an authoritative response (RFC 6762 section 18.4). */
ASSERT_OK_POSITIVE(dns_packet_validate_reply(*p));
ASSERT_EQ(DNS_PACKET_QR(*p), 1);
ASSERT_EQ(DNS_PACKET_AA(*p), 1);
ASSERT_OK(dns_packet_extract(*p));
DNS_ANSWER_FOREACH(rr, (*p)->answer)
ASSERT_OK(strv_extend(&names, dns_resource_key_name(rr->key)));
}
dns_packet_unref_array(packets, n_packets);
return TAKE_PTR(names);
}
/* The RFC 6762 section 17 ceiling minus the headers each family puts on the wire. Spelled out rather
* than re-derived from udp_header_size(): a test that recomputes the formula under test only pins
* that it was applied twice the same way. IPv6 additionally loses the 8-byte Fragment header. */
#define EXPECTED_FRAGMENTED_MAX_IPV4 (9000U - 20U - 8U)
#define EXPECTED_FRAGMENTED_MAX_IPV6 (9000U - 40U - 8U - 8U)
TEST(mdns_announcement_max_sizes_without_a_known_mtu) {
size_t max_size, fragmented_max;
/* No MTU known yet: cut to what a host of the family must accept, not to section 17's
* ceiling, which is the bound for a datagram deliberately fragmented to carry one record.
* The two must stay apart or the lone-oversized-record path is unreachable. */
mdns_announcement_max_sizes(AF_INET, /* link_mtu= */ 0, &max_size, &fragmented_max);
ASSERT_EQ(fragmented_max, (size_t) EXPECTED_FRAGMENTED_MAX_IPV4);
ASSERT_EQ(max_size, (size_t) (MDNS_PACKET_UNKNOWN_MTU_IPV4 - 20U - 8U));
ASSERT_LT(max_size, fragmented_max);
mdns_announcement_max_sizes(AF_INET6, /* link_mtu= */ 0, &max_size, &fragmented_max);
ASSERT_EQ(fragmented_max, (size_t) EXPECTED_FRAGMENTED_MAX_IPV6);
ASSERT_EQ(max_size, (size_t) (IPV6_MIN_MTU - 40U - 8U));
ASSERT_LT(max_size, fragmented_max);
}
TEST(mdns_announcement_max_sizes_from_the_link_mtu) {
size_t max_size, fragmented_max;
/* An Ethernet link: packets are cut to the MTU, while a lone oversized record may still rely
* on fragmentation up to the ceiling. */
mdns_announcement_max_sizes(AF_INET, /* link_mtu= */ 1500, &max_size, &fragmented_max);
ASSERT_EQ(max_size, 1500U - 20U - 8U);
ASSERT_EQ(fragmented_max, (size_t) EXPECTED_FRAGMENTED_MAX_IPV4);
mdns_announcement_max_sizes(AF_INET6, /* link_mtu= */ 1500, &max_size, &fragmented_max);
ASSERT_EQ(max_size, 1500U - 40U - 8U);
ASSERT_EQ(fragmented_max, (size_t) EXPECTED_FRAGMENTED_MAX_IPV6);
}
TEST(mdns_announcement_max_sizes_caps_a_jumbo_mtu) {
size_t max_size, fragmented_max;
/* A jumbo-frame link would let an unfragmented packet exceed the section 17 ceiling, so the
* ceiling has to bind the MTU-derived size too -- this is the case the cap exists for.
* DNS_PACKET_SIZE_MAX is larger still, so nothing may derive a size from it either. */
ASSERT_GT((size_t) DNS_PACKET_SIZE_MAX, (size_t) MDNS_PACKET_FRAGMENTED_SIZE_MAX);
mdns_announcement_max_sizes(AF_INET, /* link_mtu= */ 9216, &max_size, &fragmented_max);
ASSERT_EQ(max_size, fragmented_max);
ASSERT_EQ(max_size, (size_t) EXPECTED_FRAGMENTED_MAX_IPV4);
mdns_announcement_max_sizes(AF_INET6, /* link_mtu= */ 9216, &max_size, &fragmented_max);
ASSERT_EQ(max_size, fragmented_max);
ASSERT_EQ(max_size, (size_t) EXPECTED_FRAGMENTED_MAX_IPV6);
}
TEST(mdns_announcement_max_sizes_ignores_an_mtu_below_a_packet_header) {
size_t max_size, fragmented_max;
int family;
/* An MTU leaving no room for a DNS header past the IP/UDP ones yields no bound at all:
* below them the subtraction underflows, at them dns_packet_new() gets less than the header
* it asserts room for. So the derivation falls back. A derivable bound too small for any
* record is a different case, kept deliberately -- see the packetization test. */
FOREACH_ARGUMENT(family, AF_INET, AF_INET6) {
size_t unknown_mtu_max, unknown_mtu_fragmented;
mdns_announcement_max_sizes(family, /* link_mtu= */ 0,
&unknown_mtu_max, &unknown_mtu_fragmented);
mdns_announcement_max_sizes(family, udp_header_size(family) + DNS_PACKET_HEADER_SIZE,
&max_size, &fragmented_max);
ASSERT_EQ(max_size, unknown_mtu_max);
ASSERT_EQ(fragmented_max, unknown_mtu_fragmented);
mdns_announcement_max_sizes(family, udp_header_size(family) - 1,
&max_size, &fragmented_max);
ASSERT_EQ(max_size, unknown_mtu_max);
ASSERT_EQ(fragmented_max, unknown_mtu_fragmented);
/* One byte of headroom does derive from the MTU, so the assertions above are not
* just restating the default. No record fits that bound; see the packetization
* test for where such a bound leads. */
mdns_announcement_max_sizes(family, udp_header_size(family) + DNS_PACKET_HEADER_SIZE + 1,
&max_size, &fragmented_max);
ASSERT_EQ(max_size, DNS_PACKET_HEADER_SIZE + 1);
}
}
TEST(mdns_announcement_packetize_empty) {
DnsPacket **packets = NULL;
size_t n_packets = SIZE_MAX;
ASSERT_OK(mdns_announcement_packetize(/* answer= */ NULL, 512, 512,
&packets, &n_packets));
ASSERT_EQ(n_packets, 0u);
ASSERT_NULL(packets);
}
TEST(mdns_announcement_packetize_splits_at_max_size) {
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
DnsPacket **packets = NULL;
size_t n_packets = 0, sz = 0;
answer = dns_answer_new(5);
ASSERT_NOT_NULL(answer);
sz = answer_add_equal_sized(answer, STRV_MAKE("small0", "small1", "small2", "small3", "small4"));
/* Exactly two records fit a packet: five records must make three packets, filled in
* order. */
ASSERT_OK(mdns_announcement_packetize(
answer,
DNS_PACKET_HEADER_SIZE + 2 * sz,
DNS_PACKET_HEADER_SIZE + 2 * sz,
&packets, &n_packets));
ASSERT_EQ(n_packets, 3u);
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[0]), 2u);
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[1]), 2u);
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[2]), 1u);
_cleanup_strv_free_ char **names = check_and_free_packets(packets, n_packets);
ASSERT_TRUE(strv_equal(names, STRV_MAKE("small0", "small1", "small2", "small3", "small4")));
}
TEST(mdns_announcement_packetize_oversized_rr_within_fragmented_ceiling) {
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *big = NULL;
DnsPacket **packets = NULL;
size_t n_packets = 0, sz = 0, big_sz;
answer = dns_answer_new(4);
ASSERT_NOT_NULL(answer);
/* small, small, big, small -- the big record does not fit a packet even alone. */
sz = answer_add_equal_sized(answer, STRV_MAKE("small0", "small1"));
big = a_rr("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
big_sz = rr_wire_size(big);
ASSERT_GT(big_sz, 2 * sz);
ASSERT_OK(dns_answer_add(answer, big, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = a_rr("small2");
ASSERT_EQ(rr_wire_size(rr), sz);
ASSERT_OK(dns_answer_add(answer, rr, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
/* The big record must ride alone in a packet sized up to the fragmented ceiling, between
* the regular packets of its neighbors. */
ASSERT_OK(mdns_announcement_packetize(
answer,
DNS_PACKET_HEADER_SIZE + 2 * sz,
DNS_PACKET_HEADER_SIZE + big_sz,
&packets, &n_packets));
ASSERT_EQ(n_packets, 3u);
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[0]), 2u);
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[1]), 1u);
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[2]), 1u);
ASSERT_EQ(packets[1]->size, DNS_PACKET_HEADER_SIZE + big_sz);
_cleanup_strv_free_ char **names = check_and_free_packets(packets, n_packets);
ASSERT_TRUE(strv_equal(names,
STRV_MAKE("small0",
"small1",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"small2")));
}
TEST(mdns_announcement_packetize_skips_rr_beyond_fragmented_ceiling) {
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *giant = NULL;
DnsPacket **packets = NULL;
size_t n_packets = 0, sz = 0;
answer = dns_answer_new(3);
ASSERT_NOT_NULL(answer);
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *first = a_rr("small0");
sz = rr_wire_size(first);
ASSERT_OK(dns_answer_add(answer, first, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
giant = a_rr("ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg");
ASSERT_GT(rr_wire_size(giant), 2 * sz);
ASSERT_OK(dns_answer_add(answer, giant, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *last = a_rr("small1");
ASSERT_EQ(rr_wire_size(last), sz);
ASSERT_OK(dns_answer_add(answer, last, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
/* With no fragmented headroom the record that fits nowhere is dropped, and its neighbors
* still go out. */
ASSERT_EQ(mdns_announcement_packetize(
answer,
DNS_PACKET_HEADER_SIZE + 2 * sz,
DNS_PACKET_HEADER_SIZE + 2 * sz,
&packets, &n_packets), 1);
ASSERT_EQ(n_packets, 2u);
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[0]), 1u);
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[1]), 1u);
/* The giant is gone; its neighbours went out intact and in order. */
_cleanup_strv_free_ char **names = check_and_free_packets(packets, n_packets);
ASSERT_TRUE(strv_equal(names, STRV_MAKE("small0", "small1")));
}
/* The packet a failed fragmented retry leaves behind must not be reused for the records that follow:
* it was built to fragmented_max, and packing them to that bound is the oversized multi-record
* announcement this whole split exists to prevent. Reaching it takes max_size < fragmented_max, so
* that the retry runs at all, together with a record too large for either bound, so that it fails. */
TEST(mdns_announcement_packetize_drops_the_widened_packet_of_a_failed_retry) {
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *giant = NULL;
DnsPacket **packets = NULL;
size_t n_packets = 0, sz = 0;
answer = dns_answer_new(4);
ASSERT_NOT_NULL(answer);
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *first = a_rr("small0");
sz = rr_wire_size(first);
/* Too large for max_size and for fragmented_max alike, so its retry fails as well. */
giant = a_rr("ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg");
ASSERT_GT(rr_wire_size(giant), 3 * sz);
ASSERT_OK(dns_answer_add(answer, giant, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
ASSERT_OK(dns_answer_add(answer, first, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
FOREACH_STRING(name, "small1", "small2") {
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = a_rr(name);
ASSERT_EQ(rr_wire_size(rr), sz);
ASSERT_OK(dns_answer_add(answer, rr, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
}
ASSERT_EQ(mdns_announcement_packetize(
answer,
DNS_PACKET_HEADER_SIZE + 2 * sz,
DNS_PACKET_HEADER_SIZE + 3 * sz,
&packets, &n_packets), 1);
/* Two packets of max_size, not one of fragmented_max -- which is exactly three records
* wide, so a reused widened packet would hold all three and show up as a single ancount. */
ASSERT_EQ(n_packets, 2u);
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[0]), 2u);
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[1]), 1u);
_cleanup_strv_free_ char **names = check_and_free_packets(packets, n_packets);
ASSERT_TRUE(strv_equal(names, STRV_MAKE("small0", "small1", "small2")));
}
/* A bound derived from a real but tiny MTU, too small for any record to fit: each record then goes
* out alone in a packet larger than the MTU, which is the one shape RFC 6762 section 17 allows to be
* fragmented: several records in one datagram would be too, and section 17 allows that for a lone
* record only. */
TEST(mdns_announcement_packetize_bound_too_small_for_any_record) {
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
DnsPacket **packets = NULL;
size_t n_packets = 0, sz = 0;
answer = dns_answer_new(3);
ASSERT_NOT_NULL(answer);
sz = answer_add_equal_sized(answer, STRV_MAKE("small0", "small1", "small2"));
ASSERT_OK(mdns_announcement_packetize(
answer,
DNS_PACKET_HEADER_SIZE + 1,
DNS_PACKET_HEADER_SIZE + 4 * sz,
&packets, &n_packets));
ASSERT_EQ(n_packets, 3u);
FOREACH_ARRAY(p, packets, n_packets) {
ASSERT_EQ(DNS_PACKET_ANCOUNT(*p), 1u);
ASSERT_EQ((*p)->size, DNS_PACKET_HEADER_SIZE + sz);
}
_cleanup_strv_free_ char **names = check_and_free_packets(packets, n_packets);
ASSERT_TRUE(strv_equal(names, STRV_MAKE("small0", "small1", "small2")));
}
/* The fragmented retry reached without a preceding seal: when the oversized record is the *first*
* item there is nothing accumulated to flush, so the retry is entered with nothing counted yet and a
* packet that had nothing appended -- a different path into the same branch than the mid-answer case
* above, and the one that pins the lone record's ancount at exactly 1. */
TEST(mdns_announcement_packetize_oversized_rr_first) {
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *giant = NULL;
DnsPacket **packets = NULL;
size_t n_packets = 0, sz = 0;
answer = dns_answer_new(3);
ASSERT_NOT_NULL(answer);
giant = a_rr("ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg");
ASSERT_OK(dns_answer_add(answer, giant, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *first = a_rr("small0");
sz = rr_wire_size(first);
ASSERT_GT(rr_wire_size(giant), 2 * sz);
ASSERT_OK(dns_answer_add(answer, first, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *last = a_rr("small1");
ASSERT_EQ(rr_wire_size(last), sz);
ASSERT_OK(dns_answer_add(answer, last, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
ASSERT_OK(mdns_announcement_packetize(
answer,
DNS_PACKET_HEADER_SIZE + 2 * sz,
DNS_PACKET_HEADER_SIZE + 4 * sz,
&packets, &n_packets));
/* The giant goes out alone in the first packet -- at its own size, so the case stays the
* one it is meant to be: too large for max_size, small enough for the fragmented bound --
* with an ancount of exactly 1, not the count of a packet that was never filled. Its
* followers share the second. */
ASSERT_EQ(n_packets, 2u);
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[0]), 1u);
ASSERT_EQ(packets[0]->size, DNS_PACKET_HEADER_SIZE + rr_wire_size(giant));
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[1]), 2u);
_cleanup_strv_free_ char **names = check_and_free_packets(packets, n_packets);
ASSERT_TRUE(strv_equal(names, STRV_MAKE(dns_resource_key_name(giant->key), "small0", "small1")));
}
/* Nothing emittable at all. The skip must not leave a half-built packet to be sealed with an ancount
* of zero, and an announcement that has nothing to say is not an error -- the caller sends what it
* gets back and would otherwise abandon the rest of a withdrawal over one unsendable record. */
TEST(mdns_announcement_packetize_emits_nothing_when_every_rr_is_oversized) {
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *giant = NULL, *giant2 = NULL;
DnsPacket **packets = NULL;
size_t n_packets = 17;
answer = dns_answer_new(2);
ASSERT_NOT_NULL(answer);
giant = a_rr("ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg");
ASSERT_OK(dns_answer_add(answer, giant, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
giant2 = a_rr("hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh");
ASSERT_OK(dns_answer_add(answer, giant2, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
ASSERT_EQ(mdns_announcement_packetize(
answer,
DNS_PACKET_HEADER_SIZE,
DNS_PACKET_HEADER_SIZE,
&packets, &n_packets), 2);
ASSERT_EQ(n_packets, 0u);
ASSERT_NULL(packets);
}
/* What a withdrawal *is* on the wire: TTL 0, and the cache-flush bit on the class of a unique
* record. Both come from the answer item's flags, which packetization has to carry through to
* dns_packet_append_rr() -- passing 0 there instead would emit positive-TTL "goodbyes" that withdraw
* nothing, and every other testcase here would still pass. */
TEST(mdns_announcement_packetize_preserves_item_flags) {
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *bye = NULL, *plain = NULL;
DnsPacket **packets = NULL;
DnsAnswerItem *item;
size_t n_packets = 0, seen = 0;
answer = dns_answer_new(2);
ASSERT_NOT_NULL(answer);
bye = a_rr("bye");
ASSERT_OK(dns_answer_add(answer, bye, /* ifindex= */ 0,
DNS_ANSWER_GOODBYE|DNS_ANSWER_CACHE_FLUSH, /* rrsig= */ NULL));
/* A negative control in the same packet: without the flags the record keeps its TTL and
* stays a shared-owner one, so the assertions below discriminate rather than hold for
* anything that parses. */
plain = a_rr("stays");
ASSERT_OK(dns_answer_add(answer, plain, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
ASSERT_OK(mdns_announcement_packetize(
answer,
DNS_PACKET_SIZE_MAX,
DNS_PACKET_SIZE_MAX,
&packets, &n_packets));
ASSERT_EQ(n_packets, 1u);
ASSERT_OK_POSITIVE(dns_packet_validate_reply(packets[0]));
ASSERT_EQ(DNS_PACKET_QR(packets[0]), 1);
ASSERT_EQ(DNS_PACKET_AA(packets[0]), 1);
ASSERT_OK(dns_packet_extract(packets[0]));
/* Reading a packet turns the wire's cache-flush bit into the *absence* of
* DNS_ANSWER_SHARED_OWNER, so that is what pins it here. */
DNS_ANSWER_FOREACH_ITEM(item, packets[0]->answer) {
if (streq(dns_resource_key_name(item->rr->key), "bye")) {
ASSERT_EQ(item->rr->ttl, 0u);
ASSERT_FALSE(FLAGS_SET(item->flags, DNS_ANSWER_SHARED_OWNER));
} else {
/* Named explicitly, so a record with neither name cannot satisfy the
* negative control by falling into this arm. */
ASSERT_STREQ(dns_resource_key_name(item->rr->key), "stays");
ASSERT_EQ(item->rr->ttl, 120u);
ASSERT_TRUE(FLAGS_SET(item->flags, DNS_ANSWER_SHARED_OWNER));
}
seen++;
}
ASSERT_EQ(seen, 2u);
dns_packet_unref_array(packets, n_packets);
}
/* A 256-byte character string cannot be encoded by any packet: its length prefix is one byte. That
* is an encoding error (-E2BIG; an rdata past 64KiB is the -ENOSPC twin), not a size one, and it
* must cost this record alone -- the encoder rolls the packet back, so the neighbours go out intact
* and in order rather than the whole announcement being abandoned over one bad record. */
TEST(mdns_announcement_packetize_skips_rr_that_cannot_be_encoded) {
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *first = NULL, *bad = NULL, *last = NULL;
DnsPacket **packets = NULL;
size_t n_packets = 0;
answer = dns_answer_new(3);
ASSERT_NOT_NULL(answer);
first = a_rr("small0");
ASSERT_OK(dns_answer_add(answer, first, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
bad = txt_rr("bad", 256);
ASSERT_OK(dns_answer_add(answer, bad, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
last = a_rr("small1");
ASSERT_OK(dns_answer_add(answer, last, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL));
ASSERT_EQ(mdns_announcement_packetize(
answer,
DNS_PACKET_SIZE_MAX,
DNS_PACKET_SIZE_MAX,
&packets, &n_packets), 1);
ASSERT_EQ(n_packets, 1u);
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[0]), 2u);
_cleanup_strv_free_ char **names = check_and_free_packets(packets, n_packets);
ASSERT_TRUE(strv_equal(names, STRV_MAKE("small0", "small1")));
}
/* DNS-SD names all share a suffix, and the compression map is per packet: one carried across a
* boundary would emit a pointer into offsets that only exist in the previous packet. The cases
* above use names sharing none, so this is where that is pinned -- every packet parses back on its
* own, and the second record of a packet did compress against the first. */
TEST(mdns_announcement_packetize_compresses_within_a_packet_only) {
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
DnsPacket **packets = NULL;
size_t n_packets = 0, sz = 0;
answer = dns_answer_new(5);
ASSERT_NOT_NULL(answer);
sz = answer_add_equal_sized(answer, STRV_MAKE("svc0._x._udp.local",
"svc1._x._udp.local",
"svc2._x._udp.local",
"svc3._x._udp.local",
"svc4._x._udp.local"));
ASSERT_OK(mdns_announcement_packetize(
answer,
DNS_PACKET_HEADER_SIZE + 2 * sz,
DNS_PACKET_HEADER_SIZE + 2 * sz,
&packets, &n_packets));
ASSERT_EQ(n_packets, 3u);
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[0]), 2u);
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[1]), 2u);
ASSERT_EQ(DNS_PACKET_ANCOUNT(packets[2]), 1u);
/* A full packet of two uncompressed records would be exactly the bound. */
ASSERT_LT(packets[0]->size, DNS_PACKET_HEADER_SIZE + 2 * sz);
ASSERT_LT(packets[1]->size, DNS_PACKET_HEADER_SIZE + 2 * sz);
_cleanup_strv_free_ char **names = check_and_free_packets(packets, n_packets);
ASSERT_TRUE(strv_equal(names,
STRV_MAKE("svc0._x._udp.local",
"svc1._x._udp.local",
"svc2._x._udp.local",
"svc3._x._udp.local",
"svc4._x._udp.local")));
}
DEFINE_TEST_MAIN(LOG_DEBUG)
+2
View File
@@ -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);
+1
View File
@@ -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);