A custom DNS proxy server written in C++ that intercepts, parses, and filters DNS queries to enforce domain blocking before forwarding allowed traffic to an upstream resolver.
Built as a project for the Network Applications course at FIT BUT (Brno University of Technology), this proxy operates entirely without high-level web frameworks. It implements the RFC 1035 specification from the ground up using raw POSIX sockets and manual binary buffer manipulation.
Because this is a headless network daemon, the best way to see it work is through the terminal.
The proxy binds to a port, loads the blocklist, and connects to an upstream resolver (e.g., Google DNS).
$ sudo ./dns -s 8.8.8.8 -f blocklist.txt -v
1730382000 - [DNSServer.start] Starting on port 53, forwarding to 8.8.8.8:53
1730382000 - [DNSFilter.load] Loaded 45 domains
1730382000 - [DNSServer.start] Server started successfullyWhen a client requests an allowed domain, the proxy safely rewrites the query ID, forwards it, and routes the response back instantly.
$ dig @127.0.0.1 cloudflare.com A +short
104.21.84.42
172.67.199.117When a blocked domain (or its subdomain) is requested, the proxy intercepts the query, skips the upstream entirely, and locally synthesizes an RFC-compliant REFUSED (RCODE 5) error packet.
$ dig @127.0.0.1 sub.example.com A
;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 45123
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;sub.example.com. IN AInstead of relying on high-level networking libraries, the server interacts with the network stack entirely through raw POSIX sockets. It processes RFC 1035 UDP messages by manually parsing binary buffers, applying bitmasks for DNS header flags, and managing network byte order (htons/ntohs).
To achieve high throughput and reliability, the architecture relies especially on:
- Dual-Stack Networking: A single
AF_INET6socket configured withIPV6_V6ONLY=0handles both legacy IPv4 (as mapped addresses) and modern IPv6 traffic seamlessly, completely avoiding duplicated socket logic. - Lockless Concurrency: By using a single-threaded, non-blocking
select()event loop, the server avoids thread-locking overhead while comfortably tracking up to 60,000 concurrent pending queries. - Burst Draining: Both sockets are set
O_NONBLOCKviafcntl, and eachselect()wakeup fully drains them with a tightwhileloop untilEAGAIN. This prevents head-of-line blocking when many packets arrive in the same wakeup. - NAT-like State Management: Because different local clients might coincidentally generate the same DNS transaction IDs, the proxy intercepts and rewrites incoming IDs to unique internal values. It maintains a memory map of these transactions to safely route upstream replies back to the exact originating client.
Acting as a transparent proxy means the server has to be resilient against both malformed network traffic and internal resource exhaustion.
- Compression Loops: DNS names are often compressed using binary pointers. The parsing engine implements strict cycle detection with hard jump limits to prevent infinite loops, neutralizing potential DoS attacks from maliciously crafted packets.
- Query Sanitization: Each packet passes a multi-gate validation chain before any domain name is touched: a minimum 12-byte size check, single-question enforcement (
FORMERRotherwise), non-QUERYopcode rejection (NOTIMP), andIN-class-only carriage (NOTIMP). Malformed or unexpected requests never reach the name parser. - Record Type Whitelist: Only
AandAAAAquery types are forwarded to the upstream. All others returnNOTIMP, blocking DNS amplification viaANYqueries and preventing data exfiltration throughTXTtunnels. - Garbage Collection: To ensure the ID mapping doesn't leak memory from dropped packets or unresponsive upstreams, an active cleanup routine fires every 2 milliseconds on the event loop. It purges queries older than 4 seconds and routes a
SERVFAILresponse back to the client. - O(1) Subdomain Matching: Rather than over-engineering a complex Trie structure, the filtering engine relies on iterative parent-domain stripping and fixed hash lookups against a normalized set. Blocking
example.cominstantly coversmail.example.comwith minimal computational overhead.
The engine expects a simple text file (-f) containing the domains to block. It automatically trims whitespace, ignores empty lines, and handles # comments.
# blocklist.txt
example.com
tracker.analytics.net
malicious-domain.org
Note: Blocking
example.comautomatically blocks all subdomains likemail.example.comora.b.c.example.com.
The proxy was stress-tested using dnsperf against Google Public DNS (8.8.8.8) to measure added latency and throughput capacity.
Under moderate load (20 concurrent clients, 1,000 QPS target), the proxy adds 0.0 ms of latency compared to direct upstream resolution, maintaining a 99.8%+ completion rate. Under heavy sustained load (20 clients, 5,000 QPS target), the proxy handles over 1,000 completed queries per second with a predictable latency increase of ~36 ms while managing the ID rewrite mapping and blocklist hash lookups.
Compilation:
make
make test # Run automated system testsRunning the Server:
# Listen on default port 53, forward to Google DNS, apply blocklist
sudo ./dns -s 8.8.8.8 -f blocklist.txt
# Listen on custom port 5353, forward to Cloudflare DNS, enable verbose logging
./dns -s 1.1.1.1 -p 5353 -f blocklist.txt -vIn accordance with the project scope, the following RFC features are explicitly not implemented:
- No DNS caching: Every allowed query requires an upstream round-trip.
- No TCP transport: Operates strictly over UDP.
- Restricted Query Types: Only
AandAAAAquery types are supported. Others returnNOTIMP. - No EDNS(0): UDP messages are strictly limited to the classic 512-byte limit.
- No DNSSEC validation: Cryptographic validation relies entirely on the upstream resolver.