From-scratch DNS filtering server written in C++20. It listens for DNS queries, drops (or rejects) queries for domains listed in a supplied blocklist, and forwards everything else — unchanged — to an upstream recursive resolver, then relays the resolver's answer back to the original client.
The DNS wire protocol (message parsing, header/question/record handling, name compression) is implemented by hand using only raw UDP sockets and the standard C/C++ libraries — no third-party DNS or networking libraries.
This was developed as a university project for the Computer Networks and Communications course at the Faculty of Information Technology, Brno University of Technology (FIT VUT), and is published here as a portfolio piece.
- DNS forwarding proxy over UDP, for
Arecord queries. - Domain blocklist with subdomain matching —
example.comalso blockswww.example.com,ads.tracker.example.com, etc. - IPv4 and IPv6 listening sockets, so clients can query either
127.0.0.1or::1. - Transparent relaying of upstream responses — they are forwarded untouched, preserving DNS name compression present in the reply.
- Hand-written DNS protocol layer — header, question, and record parsing; support for domain-name label compression per RFC 1035.
- Retry and timeout handling — queries that receive no answer from the
upstream resolver within a configurable delay are retransmitted up to a
configurable number of times (
-r/-textensions). - Graceful failure modes — malformed messages, unsupported query types, and
refused domains produce appropriate DNS
RCODEresponses (FORMERR,SERVFAIL,NOTIMP,REFUSED). - Verbose mode (
-v) that prints per-query tracing information. - Inline ID remapping — each upstream query is issued with a locally allocated 16-bit ID so the proxy can multiplex many concurrent clients.
- Unit and integration tests, wired up to run via
make test, plus a CI workflow on GitHub Actions.
- A C++20 compiler (e.g.
g++ 12+) - GNU
make dig(for the integration test)
make dns # build the server binary ./dns
make debug # build with -g, extra warnings, and -Werror
make test # build and run unit tests + integration tests
make clean # remove build artifacts and the binaryUsage: dns -s server [-p port] -f filter_file [-v] [-r max_retries] [-t retry_delay_ms]
| Flag | Description |
|---|---|
-s server |
IP address or hostname of the upstream DNS resolver (required). |
-f filter_file |
Path to the blocklist file (required). |
-p port |
Local UDP port to listen on. Defaults to 53. |
-v |
Verbose — log every query/answer to stdout. |
-r max_retries |
Number of retransmissions to the upstream before giving up. |
-t retry_delay_ms |
Milliseconds to wait for an upstream answer before retrying. |
-h |
Show help. |
Flags may be passed in any order.
Run on a non-privileged port, forwarding to Cloudflare's resolver and filtering
blacklist.txt:
./dns -s 1.1.1.1 -p 5400 -f blacklist.txt -vThen, in another terminal:
dig @127.0.0.1 -p 5400 example.com A # allowed -> forwarded
dig @127.0.0.1 -p 5400 badexample.com A # blocked -> refusedOne domain per line. Blank lines and lines starting with # are ignored. The
file may use Unix, Windows, or classic Mac OS line endings. Surrounding
whitespace on a line is tolerated. Malformed lines are skipped with a warning on
stderr so the operator knows the file needs fixing.
# ads and trackers
doubleclick.net
ads.example.com
.
├── Makefile
├── include/ # public headers
│ ├── arg_parser.h
│ ├── dns_header.h
│ ├── dns_message.h
│ ├── dns_question.h
│ ├── dns_record.h
│ ├── dns_server.h
│ ├── filter_domains.h
│ ├── udp_socket.h
│ └── utilz.h
├── src/ # implementation (one translation unit per module)
│ ├── arg_parser.cpp
│ ├── dns_header.cpp
│ ├── dns_message.cpp
│ ├── dns_question.cpp
│ ├── dns_record.cpp
│ ├── dns_server.cpp
│ ├── filter_domains.cpp
│ ├── main.cpp
│ ├── udp_socket.cpp
│ └── utilz.cpp
└── tests/ # unit tests (C++) and integration test (shell + python)
main— argument parsing, wiring up the filter and the server.arg_parser—getopt-based CLI parsing with validation.filter_domains— loads and stores the blocklist; answers "is this domain blocked?" including subdomain checks.dns_server— the event loop: receives client queries, decides block/forward, tracks in-flight upstream queries, relays answers, handles retransmissions and timeouts.dns_message/dns_header/dns_question/dns_record— the hand-written DNS parser/serializer (RFC 1035).udp_socket— thin RAII wrapper aroundsocket(2)/bind(2)/recvfrom(2)/sendto(2)for IPv4, IPv6, and the upstream socket.utilz— small helpers (domain utilities, splitting, subdomain check).
make test runs:
- Unit tests (in
tests/test_*.cpp) covering domain validation, subdomain matching, label encode/decode, the blocklist predicate, and DNS name-to-string conversion. - Integration tests (
tests/integration_test.sh) — starts the server against a real upstream anddigs it from multiple address families, with both an unreachable and a reachable resolver to exercise retransmission. - A Python/scapy-based black-box test (
tests/test_dns_python.py) that crafts raw DNS packets.
CI (.github/workflows/test.yml) runs the same pipeline on push to main.
- [RFC 1035] Domain names — implementation and specification
- [RFC 1123] §2.1 — relaxation of the "domain names start with a letter" requirement
- [RFC 3696] §2 — further guidance on domain-name syntax
This project was created for academic coursework. Feel free to read and learn from it; please don't submit it as your own work.