Skip to content
csirikkPublic

About

C++ DNS proxy that parses RFC 1035 on raw sockets, blocks domains locally, and added 0.0 ms of latency at 1,000 queries per second in a dnsperf run.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

110 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DNS Filtering Proxy Server

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.

Pipeline

Because this is a headless network daemon, the best way to see it work is through the terminal.

1. Starting the Server

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 successfully

2. Allowed Queries (Transparent Proxying)

When 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.117

3. Blocked Queries (Policy Enforcement)

When 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 A

Architecture

Instead 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_INET6 socket configured with IPV6_V6ONLY=0 handles 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_NONBLOCK via fcntl, and each select() wakeup fully drains them with a tight while loop until EAGAIN. 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.

Security

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 (FORMERR otherwise), non-QUERY opcode rejection (NOTIMP), and IN-class-only carriage (NOTIMP). Malformed or unexpected requests never reach the name parser.
  • Record Type Whitelist: Only A and AAAA query types are forwarded to the upstream. All others return NOTIMP, blocking DNS amplification via ANY queries and preventing data exfiltration through TXT tunnels.
  • 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 SERVFAIL response 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.com instantly covers mail.example.com with minimal computational overhead.

Blocklist Format

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.com automatically blocks all subdomains like mail.example.com or a.b.c.example.com.

Performance

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.

Usage

Compilation:

make
make test # Run automated system tests

Running 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 -v

Known Limitations

In accordance with the project scope, the following RFC features are explicitly not implemented:

  1. No DNS caching: Every allowed query requires an upstream round-trip.
  2. No TCP transport: Operates strictly over UDP.
  3. Restricted Query Types: Only A and AAAA query types are supported. Others return NOTIMP.
  4. No EDNS(0): UDP messages are strictly limited to the classic 512-byte limit.
  5. No DNSSEC validation: Cryptographic validation relies entirely on the upstream resolver.

About

C++ DNS proxy that parses RFC 1035 on raw sockets, blocks domains locally, and added 0.0 ms of latency at 1,000 queries per second in a dnsperf run.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages