]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
resolved: let's track fragment sizes of servers/retry on fragmenting
authorLennart Poettering <lennart@poettering.net>
Mon, 16 Nov 2020 22:26:37 +0000 (23:26 +0100)
committerLennart Poettering <lennart@poettering.net>
Thu, 18 Feb 2021 14:55:58 +0000 (15:55 +0100)
Fragmenting sucks, let's avoid it. Thus let's start tracking the maximum
fragment size we receive.

Also, let's redo a transaction via TCP if we see fragmenting on UDP, as
effective mitigation against DNS fragment attacks.

src/resolve/resolved-dns-packet.c
src/resolve/resolved-dns-packet.h
src/resolve/resolved-dns-server.c
src/resolve/resolved-dns-server.h
src/resolve/resolved-dns-transaction.c

index 6db82ba217fd876436930acda44726ebe79df8eb..9d50336c3fe0f83c5a89ccde2961d0446269b25e 100644 (file)
@@ -2626,6 +2626,20 @@ int dns_packet_has_nsid_request(DnsPacket *p) {
         return has_nsid;
 }
 
+size_t dns_packet_size_unfragmented(DnsPacket *p) {
+        assert(p);
+
+        if (p->fragsize == 0) /* Wasn't fragmented */
+                return p->size;
+
+        /* The fragment size (p->fragsize) covers the whole (fragmented) IP packet, while the regular packet
+         * size (p->size) only covers the DNS part. Thus, subtract the UDP header from the largest fragment
+         * size, in order to determine which size of DNS packet would have gone through without
+         * fragmenting. */
+
+        return LESS_BY(p->fragsize, udp_header_size(p->family));
+}
+
 static const char* const dns_rcode_table[_DNS_RCODE_MAX_DEFINED] = {
         [DNS_RCODE_SUCCESS] = "SUCCESS",
         [DNS_RCODE_FORMERR] = "FORMERR",
index 1d53fcfb20adea4b8a50a28cc96ed0cf25bbc041..7b2abe3e7643bb2c77ff1180dce713650c72d322 100644 (file)
@@ -152,6 +152,14 @@ static inline bool DNS_PACKET_VERSION_SUPPORTED(DnsPacket *p) {
         return DNS_RESOURCE_RECORD_OPT_VERSION_SUPPORTED(p->opt);
 }
 
+static inline bool DNS_PACKET_IS_FRAGMENTED(DnsPacket *p) {
+        assert(p);
+
+        /* For ingress packets: was this packet fragmented according to our knowledge? */
+
+        return p->fragsize != 0;
+}
+
 /* LLMNR defines some bits differently */
 #define DNS_PACKET_LLMNR_C(p) DNS_PACKET_AA(p)
 #define DNS_PACKET_LLMNR_T(p) DNS_PACKET_RD(p)
@@ -325,3 +333,5 @@ static inline size_t udp_header_size(int af) {
                 assert_not_reached("Unexpected address family");
         }
 }
+
+size_t dns_packet_size_unfragmented(DnsPacket *p);
index 6a5a466af66fa5f6620a51dd5c040015bdaf762a..7b7e50092fc0b0418ca4c08da0cf014210903561 100644 (file)
@@ -255,7 +255,7 @@ static void dns_server_reset_counters(DnsServer *s) {
          * incomplete. */
 }
 
-void dns_server_packet_received(DnsServer *s, int protocol, DnsServerFeatureLevel level, size_t size) {
+void dns_server_packet_received(DnsServer *s, int protocol, DnsServerFeatureLevel level, size_t fragsize) {
         assert(s);
 
         if (protocol == IPPROTO_UDP) {
@@ -289,10 +289,10 @@ void dns_server_packet_received(DnsServer *s, int protocol, DnsServerFeatureLeve
 
         dns_server_verified(s, level);
 
-        /* Remember the size of the largest UDP packet we received from a server, we know that we can always
-         * announce support for packets with at least this size. */
-        if (protocol == IPPROTO_UDP && s->received_udp_packet_max < size)
-                s->received_udp_packet_max = size;
+        /* Remember the size of the largest UDP packet fragment we received from a server, we know that we
+         * can always announce support for packets with at least this size. */
+        if (protocol == IPPROTO_UDP && s->received_udp_fragment_max < fragsize)
+                s->received_udp_fragment_max = fragsize;
 }
 
 void dns_server_packet_lost(DnsServer *s, int protocol, DnsServerFeatureLevel level) {
@@ -389,6 +389,19 @@ void dns_server_packet_do_off(DnsServer *s, DnsServerFeatureLevel level) {
         s->packet_do_off = true;
 }
 
+void dns_server_packet_udp_fragmented(DnsServer *s, size_t fragsize) {
+        assert(s);
+
+        /* Invoked whenever we got a fragmented UDP packet. Let's do two things: keep track of the largest
+         * fragment we ever received from the server, and remember this, so that we can use it to lower the
+         * advertised packet size in EDNS0 */
+
+        if (s->received_udp_fragment_max < fragsize)
+                s->received_udp_fragment_max = fragsize;
+
+        s->packet_fragmented = true;
+}
+
 static bool dns_server_grace_period_expired(DnsServer *s) {
         usec_t ts;
 
@@ -607,7 +620,7 @@ int dns_server_adjust_opt(DnsServer *server, DnsPacket *packet, DnsServerFeature
         if (level == DNS_SERVER_FEATURE_LEVEL_LARGE)
                 packet_size = DNS_PACKET_UNICAST_SIZE_LARGE_MAX;
         else
-                packet_size = server->received_udp_packet_max;
+                packet_size = server->received_udp_fragment_max;
 
         return dns_packet_append_opt(packet, packet_size, edns_do, /* include_rfc6975 = */ true, NULL, 0, NULL);
 }
@@ -919,7 +932,7 @@ void dns_server_reset_features(DnsServer *s) {
         s->verified_feature_level = _DNS_SERVER_FEATURE_LEVEL_INVALID;
         s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_BEST;
 
-        s->received_udp_packet_max = DNS_PACKET_UNICAST_SIZE_MAX;
+        s->received_udp_fragment_max = DNS_PACKET_UNICAST_SIZE_MAX;
 
         s->packet_bad_opt = false;
         s->packet_rrsig_missing = false;
@@ -979,7 +992,7 @@ void dns_server_dump(DnsServer *s, FILE *f) {
         fputc('\n', f);
 
         fprintf(f,
-                "\tMaximum UDP packet size received: %zu\n"
+                "\tMaximum UDP fragment size received: %zu\n"
                 "\tFailed UDP attempts: %u\n"
                 "\tFailed TCP attempts: %u\n"
                 "\tSeen truncated packet: %s\n"
@@ -987,7 +1000,7 @@ void dns_server_dump(DnsServer *s, FILE *f) {
                 "\tSeen RRSIG RR missing: %s\n"
                 "\tSeen invalid packet: %s\n"
                 "\tServer dropped DO flag: %s\n",
-                s->received_udp_packet_max,
+                s->received_udp_fragment_max,
                 s->n_failed_udp,
                 s->n_failed_tcp,
                 yes_no(s->packet_truncated),
index 304d608f7a37faaa8dd245b0c69c191e9c7d200f..ccc109bc834a4236816bfc0d57f8ab00aedfac8a 100644 (file)
@@ -75,7 +75,7 @@ struct DnsServer {
         DnsServerFeatureLevel verified_feature_level;
         DnsServerFeatureLevel possible_feature_level;
 
-        size_t received_udp_packet_max;
+        size_t received_udp_fragment_max;   /* largest packet or fragment (without IP/UDP header) we saw so far */
 
         unsigned n_failed_udp;
         unsigned n_failed_tcp;
@@ -86,6 +86,7 @@ struct DnsServer {
         bool packet_rrsig_missing:1;    /* Set when RRSIG was missing */
         bool packet_invalid:1;          /* Set when we failed to parse a reply */
         bool packet_do_off:1;           /* Set when the server didn't copy DNSSEC DO flag from request to response */
+        bool packet_fragmented:1;       /* Set when we ever saw a fragmented packet */
 
         usec_t verified_usec;
         usec_t features_grace_period_usec;
@@ -118,7 +119,7 @@ DnsServer* dns_server_unref(DnsServer *s);
 void dns_server_unlink(DnsServer *s);
 void dns_server_move_back_and_unmark(DnsServer *s);
 
-void dns_server_packet_received(DnsServer *s, int protocol, DnsServerFeatureLevel level, size_t size);
+void dns_server_packet_received(DnsServer *s, int protocol, DnsServerFeatureLevel level, size_t fragsize);
 void dns_server_packet_lost(DnsServer *s, int protocol, DnsServerFeatureLevel level);
 void dns_server_packet_truncated(DnsServer *s, DnsServerFeatureLevel level);
 void dns_server_packet_rrsig_missing(DnsServer *s, DnsServerFeatureLevel level);
@@ -126,6 +127,7 @@ void dns_server_packet_bad_opt(DnsServer *s, DnsServerFeatureLevel level);
 void dns_server_packet_rcode_downgrade(DnsServer *s, DnsServerFeatureLevel level);
 void dns_server_packet_invalid(DnsServer *s, DnsServerFeatureLevel level);
 void dns_server_packet_do_off(DnsServer *s, DnsServerFeatureLevel level);
+void dns_server_packet_udp_fragmented(DnsServer *s, size_t fragsize);
 
 DnsServerFeatureLevel dns_server_possible_feature_level(DnsServer *s);
 
index 24f006be5a4d1b8eea3e825a9047e247955e4ef7..260ce76b98c7080258db5f398f450024aa25924a 100644 (file)
@@ -1031,6 +1031,7 @@ static int dns_transaction_fix_rcode(DnsTransaction *t) {
 }
 
 void dns_transaction_process_reply(DnsTransaction *t, DnsPacket *p, bool encrypted) {
+        bool retry_with_tcp = false;
         int r;
 
         assert(t);
@@ -1193,9 +1194,29 @@ void dns_transaction_process_reply(DnsTransaction *t, DnsPacket *p, bool encrypt
                         return;
                 }
 
+                /* Response was truncated, let's try again with good old TCP */
                 log_debug("Reply truncated, retrying via TCP.");
+                retry_with_tcp = true;
 
-                /* Response was truncated, let's try again with good old TCP */
+        } else if (t->scope->protocol == DNS_PROTOCOL_DNS &&
+                   DNS_PACKET_IS_FRAGMENTED(p)) {
+
+                /* Report the fragment size, so that we downgrade from LARGE to regular EDNS0 if needed */
+                if (t->server)
+                        dns_server_packet_udp_fragmented(t->server, dns_packet_size_unfragmented(p));
+
+                if (t->current_feature_level > DNS_SERVER_FEATURE_LEVEL_UDP) {
+                        /* Packet was fragmented. Let's retry with TCP to avoid fragmentation attack
+                         * issues. (We don't do that on the lowest feature level however, since crappy DNS
+                         * servers often do not implement TCP, hence falling back to TCP on fragmentation is
+                         * counter-productive there.) */
+
+                        log_debug("Reply fragmented, retrying via TCP.");
+                        retry_with_tcp = true;
+                }
+        }
+
+        if (retry_with_tcp) {
                 r = dns_transaction_emit_tcp(t);
                 if (r == -ESRCH) {
                         /* No servers found? Damn! */
@@ -1296,8 +1317,10 @@ void dns_transaction_process_reply(DnsTransaction *t, DnsPacket *p, bool encrypt
                 if (DNS_PACKET_DO(t->sent) && !DNS_PACKET_DO(t->received))
                         dns_server_packet_do_off(t->server, t->current_feature_level);
 
-                /* Report that we successfully received a packet */
-                dns_server_packet_received(t->server, p->ipproto, t->current_feature_level, p->size);
+                /* Report that we successfully received a packet. We keep track of the largest packet
+                 * size/fragment size we got. Which is useful for announcing the EDNS(0) packet size we can
+                 * receive to our server. */
+                dns_server_packet_received(t->server, p->ipproto, t->current_feature_level, dns_packet_size_unfragmented(p));
         }
 
         /* See if we know things we didn't know before that indicate we better restart the lookup immediately. */