DNS/DHCP(at)Work

DNS/DHCP(at)Work / Technical description

DNS/DHCP(at)Work

Structure, lookup order, rules, DHCP logic, web API, security and files of DNS/DHCP(at)Work, a DNS and DHCP server for test networks.

Version 0.1.0 (draft)Revised 2026-10-11DNS, DHCP and web GUI

1. Purpose

DNS/DHCP(at)Work is a DNS server and a DHCPv4 server for closed test networks and labs. It answers from its own zones and records, and it can give wrong answers on purpose – NXDOMAIN, SERVFAIL, REFUSED, no answer, slow answers, truncated answers or other addresses – so you can test how equipment and software handle failures in name lookups and address assignment.

It is delivered as an offline package for Rocky Linux 9 and uses only Python 3.9+ and the standard library. It does not use dnsmasq, BIND or Kea.

2. Structure

One Python process (python3 -m dnsdhcp serve) with three parts that share the settings and the event log: the DNS server, the DHCP server and the web GUI.

                 +--------------------------- dns-dhcp-tester.service ---------------------------+
 DNS clients  -> | UDP/TCP 53   dns_server.py  -- rules -> records -> PTR -> zones -> forwarding   |
 DHCP clients -> | UDP 67       dhcp_server.py -- address choice, leases, fault mode --> names to DNS|
 browser      -> | TCP 8053     web.py         -- JSON API + static/ (EN/NO)                         |
                 |              store.py (config.json)   eventlog.py (events.jsonl)                  |
                 +-----------------------------------------------------------------------------------+
Module Contents
dnsproto.py DNS messages (RFC 1035, 3596 AAAA, 2782 SRV, 6891 EDNS): parse queries with name compression and loop protection, build answers, truncate (TC) when the answer is too large
dns_server.py UDP and TCP listeners (2-byte length prefix), pool of 64 threads, rules, lookup, forwarding, statistics
dhcpproto.py DHCPv4 packets (RFC 2131/2132), options longer than 255 bytes (RFC 3396), client side for the self test and probe
dhcp_server.py DISCOVER/OFFER/REQUEST/ACK/NAK, RELEASE, DECLINE, INFORM, relay (giaddr), reservations, fault modes
store.py defaults, validation of every setting, atomic JSON saving (tmp + rename)
eventlog.py ring buffer of 5000 events in memory and events.jsonl on disk with rotation (5 × 10 MB)
web.py HTTP server, JSON API, login, CSRF protection and security headers
static/ the web GUI: HTML, CSS and JavaScript in English and Norwegian, no external files

The settings are read again for every query and every DHCP message, so changes to records, rules, pool and fault mode apply at once. Only a new address, port or interface restarts a listener.

3. DNS

3.1. Order for a query

  1. Rules, top to bottom. The first rule that matches the name (pattern with * and ?), the type and the client network decides.
  2. Own data: the records and the names from DHCP (leases and reservations with a host name). First the exact name, otherwise the closest *. wildcard. CNAMEs are followed up to 8 steps. A name that exists without the type asked for gives NODATA (NOERROR without answers) with the SOA.
  3. Automatic PTR from the A and AAAA records, for in-addr.arpa and ip6.arpa.
  4. Own zones: a name in a zone that does not exist gives NXDOMAIN with the SOA. SOA and NS for the zone apex are made automatically.
  5. Other names are forwarded to the configured servers (UDP, 2 s timeout per server). Without forwarding the server answers REFUSED, or NXDOMAIN or SERVFAIL if chosen.

Answers from own data and zones have the AA flag set. RA is set when forwarding is configured.

3.2. Rules

Action Answer
NXDOMAIN, SERVFAIL, REFUSED that response code at once (or after the delay)
DROP no answer; the query is counted as “not answered”
DELAY the normal lookup, sent after delay ms
TRUNCATE an empty answer with the TC flag over UDP; over TCP the query gets the normal lookup
ANSWER A and/or AAAA answers with the addresses in the rule, with the AA flag

Delay ms (0–30 000) applies to every action. The clients field takes addresses and networks (CIDR) for IPv4 and IPv6. The rule that matched is shown in the log and counted in the statistics.

3.3. Answer size and TCP

Over UDP the limit is 512 bytes, or the EDNS size the client announces (512–4096). A larger answer is sent without records and with the TC flag, and the client asks again over TCP. When the query has an OPT record, the answer has one too (UDP size 1232). TCP uses the 2-byte length prefix (RFC 1035 4.2.2), and several queries can be sent on the same connection.

3.4. Record types

Type Value Check
A / AAAA IPv4 / IPv6 address valid address
CNAME, NS, PTR domain name valid name
MX preference name 0–65535 and a valid name
SRV priority weight port target three numbers 0–65535 and a valid name
TXT text split into 255-byte strings

4. DHCP

  • Address choice: reservation → the client’s existing lease → requested address (option 50) → the first free address in the pool.
  • An offer is held for 60 s. An address a client rejects with DECLINE is held back for 600 s.
  • A REQUEST with another server’s identifier frees the offer. When the server is authoritative, a client asking for an address that is not valid here gets a NAK.
  • Replies go to the relay agent (giaddr, port 67), to the client’s address (ciaddr, port 68) on renewal, and otherwise as a broadcast to 255.255.255.255, port 68.
  • Options: 1 subnet mask, 3 router, 6 DNS, 12 host name, 15 domain, 28 broadcast address, 42 NTP, 51 lease time, 54 server identifier, 58 T1, 59 T2, 66 TFTP server, 67 boot file.
  • Without configured DNS servers, clients get the server’s own address when the DNS server is on.
  • With an interface chosen, the listener is bound with SO_BINDTODEVICE, and the server address is taken from the interface (SIOCGIFADDR). Otherwise the address towards the router is used.
  • Leases are stored in leases.json and survive a restart.
Fault mode Behaviour
normal normal DHCP
silent messages are logged but not answered
nak DISCOVER gets an OFFER, but every REQUEST gets a NAK
delay every reply is sent after delay ms (0–60 000)
exhausted DISCOVER is not answered, as if the pool were full

DHCP clients that send a host name (option 12) get the name <host name>.<domain> in DNS while the lease is valid. Reservations with a host name are always included.

5. Settings and validation

The settings are stored in /var/lib/dns-dhcp-tester/config.json, with one part for DNS and one for DHCP. Everything from the web GUI is checked before it is saved. An error gives HTTP 400 with a readable message, and nothing is saved.

  • Names, zones and name patterns are checked against allowed characters and lengths.
  • Addresses, networks, ports, TTLs, delays and lease time are checked against valid ranges.
  • The DHCP pool must be within one network, the start must come before the end, and the pool can have at most 65 536 addresses. Reservations must be in the network, and no MAC or IP address can be used twice.
  • If the file is damaged or invalid at start-up, the defaults are used and a warning is shown in the web GUI and the log.

6. Web API

Every call requires login. Every POST also requires the header X-Requested-With: dnsdhcp-ui.

Method Path Contents
GET /api/status version, state and counters for DNS and DHCP, network interfaces
GET /api/config all settings and allowed choices
POST /api/config/dns, /api/config/dhcp save settings
GET /api/dhcp/leases leases
POST /api/dhcp/leases/delete {"mac": "..."}
POST /api/dns/test {"name", "type", "client"} – the answer without sending anything on the network
GET /api/log?since=ID&kind=dns\|dhcp\|system events after ID
POST /api/log/clear clear the view and the counters
GET /api/log/download events.jsonl

7. Security

  • Login: HTTP Basic with the user admin. The password is stored as PBKDF2-SHA256 with 200 000 rounds and a random salt. Ten wrong passwords within a minute from one address give a one-minute pause.
  • CSRF: a POST without the header X-Requested-With: dnsdhcp-ui is rejected with 403. Other web sites cannot set the header.
  • Headers: Content-Security-Policy without inline scripts and styles and without external sources, X-Frame-Options DENY, nosniff, no-referrer and no-store.
  • The service runs as the system user dnsdhcp with only CAP_NET_BIND_SERVICE (ports 53 and 67) and CAP_NET_RAW (binding to an interface), and with full systemd hardening. It can only write to /var/lib/dns-dhcp-tester.
  • DHCP is off after installation, and the web GUI asks before it is turned on.
  • The web GUI uses HTTP. Limit access with install.sh --admin-net <network> or a separate management network.

8. Files and service

Path Contents Owner
/opt/dns-dhcp-tester/app/ the program root
/opt/dns-dhcp-tester/tools/ selftest.py, probe.py root
/opt/dns-dhcp-tester/docs/ user manual and technical description root
/var/lib/dns-dhcp-tester/config.json settings dnsdhcp, 0640
/var/lib/dns-dhcp-tester/leases.json DHCP leases dnsdhcp
/var/lib/dns-dhcp-tester/log/events.jsonl event log dnsdhcp
/etc/dns-dhcp-tester/auth password hash root:dnsdhcp, 0640
/etc/dns-dhcp-tester/initial-password the password from the installation root, 0600
/etc/dns-dhcp-tester/install.conf the choices of the last install.sh root
/etc/systemd/system/dns-dhcp-tester.service the service root

install.sh checks the package checksums and that the ports are free, shows which process uses port 53, 8053 or 67 if any, opens the ports in firewalld or ufw and ends by checking the web GUI and DNS. Running install.sh again is an upgrade that keeps settings, leases, log and password.

9. Port map of the (at)Work tools

Tool Web Other ports
Iperf(at)Work 80 5201/5202
Multicast(at)Work 5152
Chrony-Time-Service(at)Work 5032
SYSLOG Receiver 8080 UDP/TCP 514, TLS 6514
SYSLOG Sender 8000
SNMP Receiver / Sender 8081 / 8082
Ping(at)Work 443
DNS/DHCP(at)Work 8053 UDP/TCP 53, UDP 67

All the tools can therefore run on the same server.

10. Self test and tools

python3 tools/selftest.py starts DNS, DHCP and the web GUI on random high ports on 127.0.0.1 and runs 44 tests without touching the service:

  • lookups of all record types, CNAME, wildcards, NXDOMAIN, NODATA, SOA/NS, PTR, truncation and EDNS;
  • every rule action, type filter, client filter and order;
  • forwarding (when the test runs as root and port 53 is free);
  • the DHCP exchange DISCOVER–OFFER–REQUEST–ACK, reservation, NAK, full pool, RELEASE, DECLINE and every fault mode;
  • validation of settings, login, CSRF protection and the API.

tools/probe.py dns <name> asks a DNS server over UDP or TCP. tools/probe.py dhcp sends a DISCOVER and shows every offer, without taking an address.

11. Limitations of the draft

  • DHCP for IPv4 and one network only. No DHCPv6.
  • DNS without DNSSEC, zone transfer (AXFR) and dynamic updates (RFC 2136).
  • Forwarding over UDP to port 53, without a cache.
  • The web GUI uses HTTP, not HTTPS.
Screenshots, installation and test scenarios: Back to the front page