From d96c288ed44f6c6dd4839bc56060180192ec8fd4 Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Sat, 5 Sep 2026 10:21:59 +0200 Subject: [PATCH 1/2] 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. --- src/resolve/resolved-dns-scope.c | 86 ++++++++----- src/resolve/resolved-mdns.c | 205 +++++++++++++++++++++++++++++++ src/resolve/resolved-mdns.h | 22 ++++ src/shared/dns-packet.c | 2 + src/shared/dns-packet.h | 1 + 5 files changed, 288 insertions(+), 28 deletions(-) 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); From b3896afea2e700cf04c5b0c62c246ce2391db747 Mon Sep 17 00:00:00 2001 From: Christian Glombek Date: Sat, 5 Sep 2026 10:21:59 +0200 Subject: [PATCH 2/2] test: add unit coverage for mDNS announcement packetization Cover the size derivation and the packing decisions that the integration test cannot reach: its veth MTU exceeds every record TEST-89 publishes, so neither the oversized-record fallback nor the drop of a record beyond the RFC 6762 section 17 ceiling is ever exercised there. The cases pin the two size bounds for both families (including the fallback for an MTU not known yet and the jumbo-frame cap), the split at max_size, the lone oversized record in a fragmented packet, the drop of a record that fits nowhere, the skip of a record that cannot be encoded at all, that answer-item flags reach the wire (a goodbye must keep TTL 0 and its cache-flush bit), and that name compression stays within a packet -- the property the split exists for, since every DNS-SD name shares a suffix and a compression pointer may not cross a packet boundary. --- src/resolve/meson.build | 3 + src/resolve/test-mdns-announce.c | 578 +++++++++++++++++++++++++++++++ 2 files changed, 581 insertions(+) create mode 100644 src/resolve/test-mdns-announce.c diff --git a/src/resolve/meson.build b/src/resolve/meson.build index 4ea7b3a4ca..b246c0c402 100644 --- a/src/resolve/meson.build +++ b/src/resolve/meson.build @@ -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'), }, diff --git a/src/resolve/test-mdns-announce.c b/src/resolve/test-mdns-announce.c new file mode 100644 index 0000000000..e95e215b6e --- /dev/null +++ b/src/resolve/test-mdns-announce.c @@ -0,0 +1,578 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#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)