From acbf761b5d22129d6eebffc6747d18414168bda0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 16 Nov 2020 23:26:37 +0100 Subject: [PATCH] resolved: let's track fragment sizes of servers/retry on fragmenting 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 | 14 ++++++++++++ src/resolve/resolved-dns-packet.h | 10 +++++++++ src/resolve/resolved-dns-server.c | 31 ++++++++++++++++++-------- src/resolve/resolved-dns-server.h | 6 +++-- src/resolve/resolved-dns-transaction.c | 29 +++++++++++++++++++++--- 5 files changed, 76 insertions(+), 14 deletions(-) diff --git a/src/resolve/resolved-dns-packet.c b/src/resolve/resolved-dns-packet.c index 6db82ba217..9d50336c3f 100644 --- a/src/resolve/resolved-dns-packet.c +++ b/src/resolve/resolved-dns-packet.c @@ -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", diff --git a/src/resolve/resolved-dns-packet.h b/src/resolve/resolved-dns-packet.h index 1d53fcfb20..7b2abe3e76 100644 --- a/src/resolve/resolved-dns-packet.h +++ b/src/resolve/resolved-dns-packet.h @@ -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); diff --git a/src/resolve/resolved-dns-server.c b/src/resolve/resolved-dns-server.c index 6a5a466af6..7b7e50092f 100644 --- a/src/resolve/resolved-dns-server.c +++ b/src/resolve/resolved-dns-server.c @@ -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), diff --git a/src/resolve/resolved-dns-server.h b/src/resolve/resolved-dns-server.h index 304d608f7a..ccc109bc83 100644 --- a/src/resolve/resolved-dns-server.h +++ b/src/resolve/resolved-dns-server.h @@ -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); diff --git a/src/resolve/resolved-dns-transaction.c b/src/resolve/resolved-dns-transaction.c index 24f006be5a..260ce76b98 100644 --- a/src/resolve/resolved-dns-transaction.c +++ b/src/resolve/resolved-dns-transaction.c @@ -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. */ -- 2.25.1