Skip to content

Pseudo Random Subdomain attack protection

Pseudo Random Subdomain and enumeration attacks are putting a heavy burden on DNS servers by sending a lot of queries for names that do not exist inside the target zone, like for example queries for ${RANDOM}.${RANDOM}.${RANDOM}.prsd.powerdns.com.

These attacks are almost always distributed over a large number of clients, and often hide between legitimate public DNS resolvers, making it hard to identify and block the clients.

Building on the blocks available in the open-source version of DNSdist, Defender provides several options to mitigate these attacks.

Sample configuration

This sample configuration will look at DNS responses from the last 60 seconds to determine whether a domain is under attack, and reply to subsequent queries for this domain wit a REFUSED answer for the next 60 seconds if it is. A detailed overview of the algorithm is available below.

---
- type: prsd
  failures:
    - "nxdomain"
    - "servfail"
    - "timeout"
    - "other-rcodes"
  maximum_queries_per_child_ratio: 2
  maximum_cache_hit_ratio: 0.2
  minimum_failure_ratio: 0.8
  minimum_number_of_children: 50
  minimum_number_of_labels: 2
  minimum_number_of_qps: 100
  include_queries_to_domain: true
  action: refused
  window: 60
  action_duration: 60
  comment: "Pseudo-Random Subdomain Attack"
  exclude_suffixes:
    - powerdns.com.
    - dnsdist.org.

With these settings, a domain needs to have at least 2 labels, and should have received, over the last 60 seconds:

  • at least 100 queries per second over the last 60 seconds
  • queries for at least 50 subdomains of this domain, over the last 60 seconds
  • queries triggering at most 20% of cache-hits
  • queries triggering a NXDomain, ServFail or other rcode different than NoError, answer, or a timeout, in 80% of the cases
  • a ratio of at least 2 queries per child

In addition, the powerdns.com and dnsdist.org domains are allow-listed and will never be considered under attack.

Please refer to reference for the complete description of the settings.

Mitigation policies

Since 2.0.0, DNSdist has the ability to set a tag when a dynamic rule matches, instead of taking an immediate decision (refuse, drop, allow, ...) as to what to with the query. This tag can be combined with the existing rules mechanism to implement more flexible policies when dealing with queries for domains that are the target of a PRSD attack. It is for example possible to apply different actions based on the client IP, the incoming protocol, or to defer the decision until after the cache has been consulted. Note that the policies are internally translated to regular DNSdist rules, and as such they are evaluated sequentially. This is particularly important when using different policies based on the client IP, as the first matching policy will be executed and might not necessarily be the best match for the client IP. It also means that rules declared in the DNSdist configuration before the Defender configuration is applied by calling setup() will be executed before the policies-related rules. If any of these regular rules stops the processing, the policies-related rules will not be executed. For this reason we recommend applying the Defender configuration before defining any custom rules in the DNSdist configuration.

Let's look at an example:

---
- type: prsd
  failures:
    - "nxdomain"
    - "servfail"
    - "timeout"
    - "other-rcodes"
  maximum_queries_per_child_ratio: 2
  maximum_cache_hit_ratio: 0.2
  minimum_failure_ratio: 0.8
  minimum_number_of_children: 50
  minimum_number_of_labels: 2
  minimum_number_of_qps: 100
  include_queries_to_domain: true
  action: drop
  window: 60
  action_duration: 60
  comment: "Pseudo-Random Subdomain Attack"
  policies:
    - protocols:
        - "DoTCP"
      action:
        type: "allow"
    - subnets:
        - "127.0.0.1/32"
      options:
        allow_cache_lookups: true
      action:
        type: "rate-limit"
        ipv4_mask: 32
        ipv6_mask: 64
        qps: 1
        rate_exceeded_action: "refused"
    - subnets:
        - "192.0.2.0/24"
      action:
        type: "slip"
        slip: 4
        action: "truncate"
    - subnets:
        - "2001:DB8::/32"
      action:
        type: "log"
    - subnets:
        - "198.51.100.0/24"
      action:
        type: "route"
        pool: "best-effort"

This configuration defines additional policies that will be applied to queries received for a domain that is considered under PRSD attack:

  • the first policy matches queries received over Do53 TCP, and allows them
  • the second policy matches queries received from 127.0.0.1/32, defer the decision until the cache lookup has been done meaning that cache-hits will always be allowed, and applies a per-subnet (/32 for IPv4, /64 for IPv6) rate-limiting action of 1 query per second to cache misses. Queries below the rate-limiting threshold will be allowed to go through while those over the threshold will be refused with a REFUSED response code
  • the third policy matches queries received from 192.0.2.0/24 and randomly allows 1 out of 4 Do53 UDP queries to go through while the other ones are truncated. Queries received over a different protocol are allowed to go through, remaining policies will not be applied but regular rules will
  • the fourth one matches queries from 2001:DB8::/32 and let them through after logging them at info level
  • the last one matches queries from 198.51.100.0/24 and route them to a different pool of servers named best-effort

Queries that have not been matched by any policy will be applied the action defined in the PRSD's action field, and in this case will be silently dropped.

Please refer to reference for the complete description of the settings.

Technical deep-dive

Let's look at how the policies described in the example of the previous section are actually implemented. The first policy was:

    - protocols:
        - "DoTCP"
      action:
        type: "allow"

This translates to the following query rules:

#   Name                             Rule                                                                                                                                  Action
0   Apply Defender's PRSD policy #1  (!(tag 'defender-prsd-1-skip-query-rules' is set to '1')) && (tag 'defender-prsd-1' is set to '1') && ((incoming protocol is DoTCP))  allow

It first checks whether the query targets a domain that is under PRSD attack, in which case the defender-prsd-1 tag will be set to 1, and that a previous PRSD policy did not already match, in which case either defender-prsd-1 will be set to 0 (no more processing) or defender-prsd-1-skip-query-rules will be set to 1 which means that PRSD policies should only be applied after a cache lookup has been done and resulted in a miss. If this is the case, and the incoming protocol is Do53 TCP, the query is allowed and the processing stops. Remaining PRSD policies will not be processed and neither will regular query rules. To ensure that other PRSD policies will not cause a problem after a cache-lookup, the following cache-miss rule is also inserted:

#   Name                                                                                 Rule                                                                     Action
0   Allow cache misses for Defender's PRSD policy #1, already applied before the lookup  (tag 'defender-prsd-1' is set to '1') && ((incoming protocol is DoTCP))  set tag 'defender-prsd-1' to value '0'

Setting the value of the defender-prsd-1 tag to 0 means that regular rules can still check the tag presence to know if the query targets a domain under PRSD attack, but PRSD policies will not be applied.

The second policy is a bit more complex:

    - subnets:
        - "127.0.0.1/32"
      options:
        allow_cache_lookups: true
      action:
        type: "rate-limit"
        ipv4_mask: 32
        ipv6_mask: 64
        qps: 1
        rate_exceeded_action: "refused"

This translates to the following query rules:

#   Name                                               Rule                                                          Action
1   Allow cache lookups for Defender's PRSD policy #2  (tag 'defender-prsd-1' is set to '1') && (Src: 127.0.0.1/32)  set tag 'defender-prsd-1-skip-query-rules' to value '1'

The conditions are very similar to the ones from the previous policy, matching on the client IP rather than the incoming protocol, but if they match they just set the defender-prsd-1-skip-query-rules tag to 1. This prevents remaining PRSD policies from being applied, but does nothing else since the allow_cache_lookups: true option instructs Defender to only apply the policy after a cache miss.

The rest of the logic is then implemented in cache-miss rules:

#   Name                                                                      Rule                                                                                                          Action
1   Apply Defender's PRSD policy #2 after cache lookup (additional selector)  (tag 'defender-prsd-1' is set to '1') && (Src: 127.0.0.1/32) && (IP (/32, /64) match for QPS over 1 burst 1)  set rcode 5
2   Stop processing Defender's PRSD policies after #2, after cache lookup     (tag 'defender-prsd-1' is set to '1') && (Src: 127.0.0.1/32)                                                  set tag 'defender-prsd-1' to value '0'

The first rule turns queries for a domain under PRSD attack coming from 127.0.0.1/32 and exceeding 1 QPS per client (/32 for IPv4, /64 for IPv6) into a REFUSED response. The second rule ensures that the queries for such a domain and coming from 127.0.0.1/32 but not exceeding the threshold (and thus not matching the previous rule) are not blocked by the remaining PRSD policies.