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.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
log_debug("Reply truncated, retrying via TCP.");
|
||||
|
||||
/* Response was truncated, let's try again with good old TCP */
|
||||
log_debug("Reply truncated, retrying via TCP.");
|
||||
retry_with_tcp = true;
|
||||
|
||||
} 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. */
|
||||
|
||||
Reference in New Issue
Block a user