AZiT

DHCP, DNS and time

Addresses, names and the clock for every network, from the firewall.

Why DNS, on the tour

On this page 4 sections

As in the last chapter, each file is copied from your laptop to /tmp on the firewall, then installed:

laptop
scp dhcpd.conf fw:/tmp/

1. DHCP#

Each network gets the firewall as its router, DNS and time server. Devices that need a fixed address are listed by their hardware address; a few networks also get a pool for everything else.

/etc/dhcpd.confDownload
Show the whole file, 81 lines
# /etc/dhcpd.conf - fw.internal
# Layout: .1 firewall, .2-.9 infra, .10-.149 fixed, .150-.199 pool (only where useful)
option domain-name "internal";
default-lease-time 86400;
max-lease-time 86400;
# ADMIN - wired admin laptop
subnet 192.168.1.0 netmask 255.255.255.0 {
	option routers 192.168.1.1;
	option domain-name-servers 192.168.1.1;
	option ntp-servers 192.168.1.1;
	range 192.168.1.150 192.168.1.199;
	host laptop { hardware ethernet @MAC@; fixed-address 192.168.1.10; }
}
# APMGMT - access point only, no pool
subnet 192.168.2.0 netmask 255.255.255.0 {
	option routers 192.168.2.1;
	option domain-name-servers 192.168.2.1;
	option ntp-servers 192.168.2.1;
	host unifi-ap { hardware ethernet @MAC@; fixed-address 192.168.2.2; }
}
# PS5 - no pool
subnet 192.168.3.0 netmask 255.255.255.0 {
	option routers 192.168.3.1;
	option domain-name-servers 9.9.9.9, 149.112.112.112;
	option ntp-servers 192.168.3.1;
	host ps5 { hardware ethernet @MAC@; fixed-address 192.168.3.10; }
}
# MEDIA - no pool
subnet 192.168.4.0 netmask 255.255.255.0 {
	option routers 192.168.4.1;
	option domain-name-servers 192.168.4.1;
	option ntp-servers 192.168.4.1;
	host kodi { hardware ethernet @MAC@; fixed-address 192.168.4.10; }
}
# SERVERS - cluster, UniFi host, Home Assistant; no pool
subnet 192.168.5.0 netmask 255.255.255.0 {
	option routers 192.168.5.1;
	option domain-name-servers 192.168.5.1;
	option ntp-servers 192.168.5.1;
	host k8s-cp1        { hardware ethernet @MAC@; fixed-address 192.168.5.10; }
	host k8s-cp2        { hardware ethernet @MAC@; fixed-address 192.168.5.20; }
	host k8s-cp3        { hardware ethernet @MAC@; fixed-address 192.168.5.30; }
	host k8s-worker1    { hardware ethernet @MAC@; fixed-address 192.168.5.40; }
	host k8s-worker2    { hardware ethernet @MAC@; fixed-address 192.168.5.50; }
	host unifi          { hardware ethernet @MAC@; fixed-address 192.168.5.60; }
	host homeassistant  { hardware ethernet @MAC@; fixed-address 192.168.5.140; }
}
# TRUSTED - phones and laptop on Wi-Fi
subnet 192.168.10.0 netmask 255.255.255.0 {
	option routers 192.168.10.1;
	option domain-name-servers 192.168.10.1;
	option ntp-servers 192.168.10.1;
	range 192.168.10.150 192.168.10.199;
	host laptop-wifi { hardware ethernet @MAC@; fixed-address 192.168.10.10; }
	host phone { hardware ethernet @MAC@; fixed-address 192.168.10.11; }
	host watch { hardware ethernet @MAC@; fixed-address 192.168.10.12; }
}
# IOT - pool for new IoT devices, client isolation on the SSID
subnet 192.168.20.0 netmask 255.255.255.0 {
	option routers 192.168.20.1;
	option domain-name-servers 192.168.20.1;
	option ntp-servers 192.168.20.1;
	range 192.168.20.150 192.168.20.199;
	host vacuum { hardware ethernet @MAC@; fixed-address 192.168.20.10; }
}
# GUEST - visitors, pool only
subnet 192.168.30.0 netmask 255.255.255.0 {
	option routers 192.168.30.1;
	option domain-name-servers 9.9.9.9, 149.112.112.112;
	option ntp-servers 192.168.30.1;
	range 192.168.30.150 192.168.30.199;
}
  • Replace every @MAC@ or delete the line. On most devices the hardware address is in the network settings; on phones, use the private address shown for this Wi-Fi network.
firewall
doas install -o root -g wheel -m 0644 /tmp/dhcpd.conf /etc/dhcpd.conf
doas dhcpd -n -c /etc/dhcpd.conf
doas rcctl enable dhcpd
doas rcctl set dhcpd flags igc1 igc2 igc3 igc4 igc5 vlan10 vlan20 vlan30
doas rcctl start dhcpd

Check

firewall
rcctl check dhcpd
doas tail -5 /var/log/daemon

dhcpd(ok), and no errors. Devices already connected start getting their addresses: DHCPACK on … lines.

Keep the laptop on its manual address until chapter 5. On DHCP it would use the firewall as its router, and the firewall cannot pass traffic to the internet until pf adds NAT. If the laptop reaches the internet another way meanwhile, such as a phone hotspot, it would lose that.

2. DNS with unbound#

unbound answers for every network: local names itself, everything else by asking the internet's name servers directly, with DNSSEC validation. In chapter 6 it starts forwarding through the tunnel instead.

/var/unbound/etc/unbound.confDownload
Show the whole file, 103 lines
# /var/unbound/etc/unbound.conf - fw.internal
server:
	interface: 127.0.0.1
	interface: 192.168.1.1
	interface: 192.168.2.1
	interface: 192.168.3.1
	interface: 192.168.4.1
	interface: 192.168.5.1
	interface: 192.168.10.1
	interface: 192.168.20.1
	interface: 192.168.30.1
	do-ip6: no
	access-control: 0.0.0.0/0 refuse
	access-control: 127.0.0.0/8 allow
	access-control: 192.168.1.0/24 allow
	access-control: 192.168.2.0/24 allow
	access-control: 192.168.3.0/24 allow
	access-control: 192.168.4.0/24 allow
	access-control: 192.168.5.0/24 allow
	access-control: 192.168.10.0/24 allow
	access-control: 192.168.20.0/24 allow
	access-control: 192.168.30.0/24 allow
	# Hardening and privacy
	hide-identity: yes
	hide-version: yes
	harden-glue: yes
	harden-dnssec-stripped: yes
	qname-minimisation: yes
	aggressive-nsec: yes
	prefetch: yes
	# DNSSEC validation
	auto-trust-anchor-file: "/var/unbound/db/root.key"
	val-log-level: 1
	# DNS rebinding protection: public names may not resolve to internal addresses
	private-address: 10.0.0.0/8
	private-address: 100.64.0.0/10
	private-address: 169.254.0.0/16
	private-address: 172.16.0.0/12
	private-address: 192.168.0.0/16
	private-domain: "internal."
	# Tell Firefox not to switch to DNS over HTTPS on its own
	local-zone: "use-application-dns.net." always_nxdomain
	# iCloud Private Relay: switch it off for this network (Apple's documented method),
	# so Safari uses unbound and the VPN's DNS instead of Apple's DNS partners
	local-zone: "mask.icloud.com." always_nxdomain
	local-zone: "mask-h2.icloud.com." always_nxdomain
	# Local names
	local-zone: "internal." static
	local-data: "fw.internal.            IN A 192.168.1.1"
	local-data: "laptop.internal.          IN A 192.168.1.10"
	local-data: "unifi-ap.internal.      IN A 192.168.2.2"
	local-data: "ps5.internal.           IN A 192.168.3.10"
	local-data: "kodi.internal.          IN A 192.168.4.10"
	local-data: "k8s-cp1.internal.       IN A 192.168.5.10"
	local-data: "k8s-cp2.internal.       IN A 192.168.5.20"
	local-data: "k8s-cp3.internal.       IN A 192.168.5.30"
	local-data: "k8s-worker1.internal.   IN A 192.168.5.40"
	local-data: "k8s-worker2.internal.   IN A 192.168.5.50"
	local-data: "unifi.internal.         IN A 192.168.5.60"
	local-data: "k8s-api.internal.       IN A 192.168.5.100"
	local-data: "homeassistant.internal. IN A 192.168.5.140"
	local-data: "siem.internal.          IN A 192.168.5.200"
	local-data: "ingress.internal.       IN A 192.168.5.201"
	# Every *.apps.internal name goes to the ingress (Traefik on .201)
	local-zone: "apps.internal." redirect
	local-data: "apps.internal. IN A 192.168.5.201"
	local-data: "laptop-wifi.internal.     IN A 192.168.10.10"
	local-data: "phone.internal.        IN A 192.168.10.11"
	local-data: "watch.internal.   IN A 192.168.10.12"
	local-data: "vacuum.internal. IN A 192.168.20.10"
	# Reverse lookups (so logs and tools show names instead of IPs)
	local-data-ptr: "192.168.1.1 fw.internal."
	local-data-ptr: "192.168.1.10 laptop.internal."
	local-data-ptr: "192.168.2.2 unifi-ap.internal."
	local-data-ptr: "192.168.3.10 ps5.internal."
	local-data-ptr: "192.168.4.10 kodi.internal."
	local-data-ptr: "192.168.5.10 k8s-cp1.internal."
	local-data-ptr: "192.168.5.20 k8s-cp2.internal."
	local-data-ptr: "192.168.5.30 k8s-cp3.internal."
	local-data-ptr: "192.168.5.40 k8s-worker1.internal."
	local-data-ptr: "192.168.5.50 k8s-worker2.internal."
	local-data-ptr: "192.168.5.60 unifi.internal."
	local-data-ptr: "192.168.5.100 k8s-api.internal."
	local-data-ptr: "192.168.5.140 homeassistant.internal."
	local-data-ptr: "192.168.5.200 siem.internal."
	local-data-ptr: "192.168.5.201 ingress.internal."
	local-data-ptr: "192.168.10.10 laptop-wifi.internal."
	local-data-ptr: "192.168.10.11 phone.internal."
	local-data-ptr: "192.168.10.12 watch.internal."
	local-data-ptr: "192.168.20.10 vacuum.internal."
remote-control:
	control-enable: yes
	control-interface: /var/run/unbound.sock
  • The two always_nxdomain zones are signal names: they tell Firefox not to switch to its own DNS over HTTPS, and Apple devices to switch off iCloud Private Relay on this network.
  • private-address stops public names from resolving to your internal addresses, a defence against DNS rebinding.
laptop
scp unbound.conf fw:/tmp/
firewall
doas install -o root -g wheel -m 0644 /tmp/unbound.conf /var/unbound/etc/unbound.conf
doas unbound-anchor -v
doas unbound-checkconf
doas rcctl enable unbound
doas rcctl start unbound

unbound-anchor fetches the DNSSEC root key into /var/unbound/db/root.key, the file the configuration points to. The rc script would do it at the first start, but unbound-checkconf runs before that and fails without it. On a fresh key it reports that the anchor was written and exits with status 1; that is expected.

Check

laptop
dig @192.168.1.1 openbsd.org +short
dig @192.168.1.1 fw.internal +short
dig @192.168.1.1 dnssec-failed.org | grep status

An address for openbsd.org, 192.168.1.1 for fw.internal, and status: SERVFAIL for the last one: that domain is signed wrongly on purpose, so validation is working.

3. The firewall uses its own DNS#

Until now the firewall has used your provider's DNS servers. From here on it asks unbound, and the DHCP client on the WAN ignores the DNS servers it is offered:

/etc/resolv.confDownload
nameserver 127.0.0.1
search internal
lookup file bind
/etc/dhcpleased.confDownload
interface igc0 {
	ignore dns
}
laptop
scp resolv.conf dhcpleased.conf fw:/tmp/
firewall
doas rcctl disable resolvd && doas rcctl stop resolvd
doas install -o root -g wheel -m 0644 /tmp/resolv.conf /etc/resolv.conf
doas install -o root -g wheel -m 0644 /tmp/dhcpleased.conf /etc/dhcpleased.conf
doas rcctl restart dhcpleased

Check

firewall
ping -c 1 openbsd.org

A reply, and /etc/resolv.conf still says nameserver 127.0.0.1 after a few minutes.

4. Time#

ntpd keeps the clock right, checks it against HTTPS servers so a forged answer cannot shift it, and serves time to every network.

/etc/ntpd.confDownload
# /etc/ntpd.conf - fw.internal
servers pool.ntp.org
server time.cloudflare.com
sensor *
# Sanity check against HTTPS servers, so a forged NTP answer can't shift the clock
constraint from "9.9.9.9"
constraints from "www.google.com"
# Serve time to the internal networks
listen on 192.168.1.1
listen on 192.168.2.1
listen on 192.168.3.1
listen on 192.168.4.1
listen on 192.168.5.1
listen on 192.168.10.1
listen on 192.168.20.1
listen on 192.168.30.1
firewall
doas install -o root -g wheel -m 0644 /tmp/ntpd.conf /etc/ntpd.conf
doas ntpd -n && doas rcctl restart ntpd

Check

firewall
ntpctl -s status

Within a few minutes: clock synced, with all peers and the constraint valid. Right after the restart it says clock unsynced while ntpd collects samples.

Clients do not have the internet yet: that needs NAT, which comes with pf in the next chapter.