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
- Rules, top to bottom. The first rule that matches
the name (pattern with
*and?), the type and the client network decides. - 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. - Automatic PTR from the A and AAAA records, for
in-addr.arpaandip6.arpa. - 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.
- 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.jsonand 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-uiis 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
dnsdhcpwith onlyCAP_NET_BIND_SERVICE(ports 53 and 67) andCAP_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.