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:
@@ -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'),
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
@@ -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