Potential DoS: DNS load makes router unresponsive with a large VPN policy-routing domain list

With a large domain list configured for VPN policy routing, elevated DNS query rates can severely degrade router performance, disrupt internet connectivity, and make SSH access difficult.

This is observed on a Flint 4 with two WireGuard tunnels. One tunnel has 29,220 domain entries and 19 IP CIDR entries configured for policy routing. In this configuration, DNS query rates above approximately 300 queries per second caused severe router performance degradation.

Although a domain list of this size may be uncommon, the results suggest a potential denial-of-service issue that becomes more pronounced as the list grows.

DNS load test and observed behavior

I used dnsload-lan.py to generate DNS queries from the LAN to the router:

python3 ./dnsload-lan.py flood 192.168.8.1 53 500 10
flood: offered=500qps secs=10.0 sent=5000 answered=3024 answered_rate=302qps loss=39.5%
dnsload-lan.py
#!/usr/bin/env python3
"""DNS load generator for the dns-mark VM test.

latency <host> <port> <count>            closed loop, one query in flight
flood   <host> <port> <rate_qps> <secs>  open loop at a fixed send rate
"""
import random
import socket
import statistics
import struct
import sys
import time


def query(qid, name):
    hdr = struct.pack(">HHHHHH", qid, 0x0100, 1, 0, 0, 0)
    q = b"".join(bytes([len(p)]) + p.encode() for p in name.split(".")) + b"\0"
    return hdr + q + struct.pack(">HH", 1, 1)


def rname():
    a = "abcdefghijklmnopqrstuvwxyz"
    return "www." + "".join(random.choice(a) for _ in range(10)) + ".lan"


def latency(host, port, count):
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.settimeout(1.0)
    rtts, lost = [], 0
    for i in range(count):
        qid = i & 0xFFFF
        t0 = time.perf_counter()
        s.sendto(query(qid, rname()), (host, port))
        try:
            while True:
                d, _ = s.recvfrom(4096)
                if struct.unpack(">H", d[:2])[0] == qid:
                    break
            rtts.append((time.perf_counter() - t0) * 1e3)
        except socket.timeout:
            lost += 1
    rtts.sort()
    p = lambda q: rtts[min(len(rtts) - 1, int(q * len(rtts)))] if rtts else float("nan")
    print(f"latency: n={count} lost={lost} median={statistics.median(rtts):.3f}ms "
          f"p90={p(0.90):.3f}ms p99={p(0.99):.3f}ms max={rtts[-1]:.3f}ms")


def flood(host, port, rate, secs):
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4 << 20)
    s.setblocking(False)
    sent = recv = 0
    interval = 1.0 / rate
    t_start = time.perf_counter()
    nxt = t_start
    end = t_start + secs
    while True:
        now = time.perf_counter()
        if now >= end:
            break
        while nxt <= now:
            try:
                s.sendto(query(sent & 0xFFFF, rname()), (host, port))
                sent += 1
            except BlockingIOError:
                pass
            nxt += interval
        try:
            while True:
                s.recvfrom(4096)
                recv += 1
        except BlockingIOError:
            pass
        time.sleep(0.0002)
    drain_end = time.perf_counter() + 1.0
    while time.perf_counter() < drain_end:
        try:
            s.recvfrom(4096)
            recv += 1
        except BlockingIOError:
            time.sleep(0.001)
    print(f"flood: offered={rate}qps secs={secs} sent={sent} answered={recv} "
          f"answered_rate={recv / secs:.0f}qps loss={100 * (1 - recv / max(sent, 1)):.1f}%")


if __name__ == "__main__":
    mode, host, port = sys.argv[1], sys.argv[2], int(sys.argv[3])
    if mode == "latency":
        latency(host, port, int(sys.argv[4]))
    else:
        flood(host, port, int(sys.argv[4]), float(sys.argv[5]))

During this test, a LAN client makes 500 DNS queries to www.<random>.lan domains per second to the router, for 10 seconds:

  • Only approximately 302 queries per second received responses, with 39.5% query loss.
  • Internet connectivity stalled.
  • SSH access became difficult.
  • System load increased dramatically (peaked to 9).

The impact therefore extended beyond DNS resolution and affected the router’s overall responsiveness and traffic forwarding.

Suspected cause

The suspected bottleneck is domain matching in the gl-sdk4-dns-mark kernel module (dns_mark.ko).

It appears that each .lan query in the test triggers a linear scan of the policy domain list, potentially checking approximately 29,220 entries per query. This could make DNS processing increasingly expensive as the domain list grows.

The hypothesis is that dns_mark.ko processing consumes enough CPU time in the networking softirq path to delay other network processing, including WAN forwarding, NAT, and VPN traffic.

My setup

board information
root@GL-BE14000:~# ubus call system board
{
        "kernel": "6.12.94",
        "hostname": "GL-BE14000",
        "system": "ARMv8 Processor rev 0",
        "model": "GL.iNet GL-BE14000",
        "board_name": "glinet,gl-be14000",
        "rootfs_type": "squashfs",
        "release": {
                "distribution": "OpenWrt",
                "version": "25.12-SNAPSHOT",
                "firmware_url": "https://downloads.openwrt.org/",
                "revision": "r32295+914-c25265953b",
                "target": "mediatek/filogic",
                "description": "OpenWrt 25.12-SNAPSHOT r32295+914-c25265953b",
                "builddate": "1791807460"
        }
}
uci show route_policy
root@GL-BE14000:~# uci show route_policy 
route_policy.global=policy
route_policy.global.enabled='1'
route_policy.global.mode='0'
route_policy.global.append_source_if='iot'
route_policy.global.instance_on='1'
route_policy.global.use_fw4='1'
route_policy.global.service_policy_en='0'
route_policy.@default[0]=default
route_policy.@default[0].type='default'
route_policy.@default[0].via='novpn'
route_policy.@default[0].name='last sort default policy'
route_policy.@default[0].from_type='device'
route_policy.@default[0].tunnel_id='100'
route_policy.@default[0].enabled='1'
route_policy.@default[0].mark='0x8000'
route_policy.gl_process=rule_process
route_policy.gl_process.name='process policy'
route_policy.gl_process.from_type='process_gid'
route_policy.gl_process.from='10000'
route_policy.gl_process.tunnel_id='201'
route_policy.gl_process.enabled='1'
route_policy.gl_process.via='novpn'
route_policy.gl_process.mark='0x8000'
route_policy.gl_process_vpn=rule_process
route_policy.gl_process_vpn.name='process policy'
route_policy.gl_process_vpn.from_type='process_gid'
route_policy.gl_process_vpn.from='20000'
route_policy.gl_process_vpn.tunnel_id='10100'
route_policy.gl_process_vpn.enabled='1'
route_policy.gl_process_vpn.mark='0x2000'
route_policy.gl_process_vpn.via='wgclient2'
route_policy.@rule[0]=rule
route_policy.@rule[0].name='Tunnel 1'
route_policy.@rule[0].tunnel_id='1174'
route_policy.@rule[0].enabled='1'
route_policy.@rule[0].killswitch='1'
route_policy.@rule[0].via_type='wireguard'
route_policy.@rule[0].group_id='5470'
route_policy.@rule[0].peer_id='2001'
route_policy.@rule[0].profiles='/etc/vpn_profiles.d/profile1174'
route_policy.@rule[0].from_type='interface'
route_policy.@rule[0].from='lan'
route_policy.@rule[0].to_type='ipset'
route_policy.@rule[0].to_manual='1'
route_policy.@rule[0].to='dst_net1174'
route_policy.@rule[0].to_list_external='/etc/domain_mac_list/dst_net1174'
route_policy.@rule[0].local_access='1'
route_policy.@rule[0].via='wgclient2'
route_policy.@rule[0].options_in_used='1'
route_policy.@rule[0].mark='0x2000'
route_policy.@rule[1]=rule
route_policy.@rule[1].name='Tunnel 2'
route_policy.@rule[1].tunnel_id='3774'
route_policy.@rule[1].enabled='1'
route_policy.@rule[1].killswitch='1'
route_policy.@rule[1].via_type='wireguard'
route_policy.@rule[1].group_id='5470'
route_policy.@rule[1].peer_id='2002'
route_policy.@rule[1].profiles='/etc/vpn_profiles.d/profile3774'
route_policy.@rule[1].from_type='interface'
route_policy.@rule[1].from='lan'
route_policy.@rule[1].to_type='ipset'
route_policy.@rule[1].to_manual='1'
route_policy.@rule[1].to='dst_net3774'
route_policy.@rule[1].to_list_external='/etc/domain_mac_list/dst_net3774'
route_policy.@rule[1].local_access='1'
route_policy.@rule[1].via='wgclient1'
route_policy.@rule[1].options_in_used='1'
route_policy.@rule[1].mark='0x1000'
policy-list counts and sample entries

Note: the policy routing web interface rejects top-level TLDs. So I manually added some top level TLDs via SSH, and the policy routing for TLDs works. Hence the entity number mismatch between web and terminal, 29165 vs 29239.

root@GL-BE14000:~# wc -l /etc/domain_mac_list/dst_net1174
39 /etc/domain_mac_list/dst_net1174

root@GL-BE14000:~# wc -l /etc/domain_mac_list/dst_net3774
29239 /etc/domain_mac_list/dst_net3774

root@GL-BE14000:~# head -n 20 /etc/domain_mac_list/dst_net3774
91.108.56.0/22
91.108.4.0/22
91.108.8.0/22
91.108.16.0/22
91.108.12.0/22
149.154.160.0/20
91.105.192.0/23
91.108.20.0/22
185.76.151.0/24
2001:b28:f23d::/48
2001:b28:f23f::/48
2001:67c:4e8::/48
2001:b28:f23c::/48
2a0a:f280::/32
192.168.142.0/24
192.168.147.0/24
172.29.211.0/24
100.64.0.0/10
fd7a:115c:a1e0::/48
tailscale.com

root@GL-BE14000:~# wc -l /tmp/dnsmasq.d/via_domain
29220 /tmp/dnsmasq.d/via_domain
DNS-mark rules reported by /proc/dns_mark/dump
root@GL-BE14000:~# cat /proc/dns_mark/dump | head
DNS Mark Rules:
Rule 1174:
  Ifnames:
    br-lan
  MACs (blacklist=1):
  Domains (blacklist=0):
    netflix.com
    netflix.net
    nflximg.com
    nflximg.net

root@GL-BE14000:~# cat /proc/dns_mark/dump | wc -l
29297

Hi,

Thank you for the detailed feedback. We’ve noted both of your reports about large VPN domain policy lists:

  1. Increased DNS response times with policy routing.

  2. Missing DNS replies and router unresponsiveness under DNS load.

We reproduced increased DNS latency and CPU usage internally. With 29,220 domain entries at 500 queries per second, network processing saturated one CPU core. However, all DNS queries received replies, and the router remained accessible.

Other CPU-intensive applications running at the same time could worsen the impact. This is a possible contributing factor, but we haven’t confirmed the cause of the missing replies and unresponsiveness in your setup.

We’ll review both reports with our product and engineering teams to evaluate performance improvements for large policy lists.

Thanks for investigating into these.

To help reproduce the behavior and assess potential performance improvements, below is my full configuration.

Please let me know which additional configuration details, logs, or measurements would be most useful. I can also run tests with individual settings mentioned below disabled to help isolate the contributing factors.

Topology

Two Flint 4 (GL-BE14000) are configured as a mesh network with wireless backhaul. The DNS load test targets the DNS server (192.168.8.1:53) on main router.

Main router configuration

In the order of the Admin Panel navbar:

  • Internet: single PPPoE WAN connection through Ethernet port 1. No SFP+ connection or multi-WAN configuration.
  • DNS: Default DNS servers supplied by the ISP and VPN tunnels. Override DNS Settings of All Clients enabled. DNS Rebinding Attack Protection enabled. Manual DNS disabled.
  • Wireless: MLO configured and enabled.
  • Cloud services: None enabled.
  • VPN: Two tunnels, with the policy-routing configuration described in my previous post.
  • Network: IPv6 enabled; Network Acceleration disabled (this and flow control settings could contribute to the observed performance difference?).
  • Flow Control:
    • Network Quality enabled.

    • Data Statistics enabled.

    • Content Filter enabled, with 298 applications selected for blocking (Malware, Gambling, all 288 Apps in Advertisement category, and DNS related: AliDNS, Apple DNS, AWS DNS, Cloudflare DNS, Duck DNS, Google DNS, NextDNS, Yandex DNS), plus 1416 custom blocked domains (DoH and HTTPDNS domains).

      excerpt of custom blocked domains
      003153.xyz
      0ms.dev
      1.dns.noridev.moe
      123000123.xyz
      1a.ns.ozer.im
      1dot1dot1dot1.cloudflare-dns.com
      2.dns.noridev.moe
      3dns.eu
      3jjxxl.tech
      5g.o0o.re
      8888.google
      abe01.dnscry.pt
      abel.waringer-atg.de
      abn01.dnscry.pt
      ad.huyct.net
      ad.justincounts.com
      ad.tzockt.beer
      ada.openbld.net
      adb-home.xaoimoon.fr
      adb.aadityakushwaha.com
      adb.timnichollsphotography.com
      adblock.dns.mullvad.net
      adblock.doh.mullvad.net
      adblock.indianets.net
      adblock.krctech.dev
      adblock.leenit.kr
      adblock.mydns.network
      adblockersite.com
      addns1.m-it.ro
      adfiltro.fun
      adfree.usableprivacy.net
      adfreedns.top
      adg.geili.me
      adg.jnorton.us
      adg.khon.dev
      adg.rueiliu.space
      adg.tshost.no
      adg.yybyy.wiki
      adguard-dns.com
      adguard-kartoffel.zernico.de
      adguard.aa4.co.uk
      adguard.abd.ong
      adguard.ajinga.net
      adguard.alu.dog
      adguard.bitteeinbyte.de
      
    • SQM enabled.

  • Security: One ACL rule blocking a Foscam device from accessing the WAN, plus nine port-forwarding rules.
  • Applications and plug-ins: None - no additional applications enabled and no apk/plug-ins installed. Application settings remain at their factory defaults.
  • All other settings remain at their defaults.

Mesh node configuration and setup sequence

I configured the main router first, including its WAN connection, Wi-Fi SSID, MLO, VPN tunnels, IPv6, IGMP snooping, Flow Control features, and port-forwarding rules.

I then added the second router as a mesh node through the Admin Panel, following the standard setup process. I made no further configuration changes on the node.

Also, occasionally, when switching the tunnel on/off/on, the nftables vpn_table could be missing, causing errors:

Sun Oct 25 08:01:58 2026 daemon.err dnsmasq\[14748\]: nftset inet vpn_table dst_net6542_6 Error: No such file or directory; did you mean table 'dns_table' in family inet?
Sun Oct 25 08:01:58 2026 daemon.err dnsmasq\[14748\]: nftset inet vpn_table dst_net6542_6 Error: No such file or directory; did you mean table 'dns_table' in family inet?

Thank you for clarifying your configuration.

We reproduced the nftset errors during VPN tunnel switching. The router removes and recreates vpn_table, then restarts dnsmasq to load the updated configuration. In our tests, the table was briefly unavailable for approximately 42–83 ms. DNS replies processed during that interval could trigger the “No such file or directory” message.

The table was restored after each switch. If these messages appear only while toggling a tunnel and DNS resolution and VPN routing work normally afterward, they can be ignored.