DHCP and NTP for the LAN with systemd-networkd

From LinuxizeNet

Handing out addresses on the LAN from the router itself, using the DHCP server built into systemd-networkd, and pushing the router as the NTP source.

Continues from Linux router with WAN, LAN and DMZ: eth1 is the LAN at 10.0.1.0/24 and the router holds 10.0.1.1.

The DHCP server[edit]

No extra package — systemd-networkd already has one. Extend the LAN file, /etc/systemd/network/20-lan.network:

[Match]
Name=eth1

[Network]
Address=10.0.1.1/24
DHCPServer=yes
ConfigureWithoutCarrier=yes

[DHCPServer]
# Pool starts at the subnet base + offset, so 10.0.1.100 - 10.0.1.199
PoolOffset=100
PoolSize=100

DefaultLeaseTimeSec=1h
MaxLeaseTimeSec=12h

# Gateway. Defaults to this interface's address, so it is only worth
# stating explicitly if the LAN's router is some other box.
EmitRouter=yes

# DNS
EmitDNS=yes
DNS=10.0.1.1

# NTP - the point of the exercise
EmitNTP=yes
NTP=10.0.1.1

Apply:

networkctl reload
networkctl status eth1
PoolOffset= counts from the base of the subnet, not from the router's address. With 10.0.1.0/24 an offset of 100 starts the pool at 10.0.1.100. Keep the pool clear of anything statically addressed.

Fixed addresses[edit]

One section per host, in the same .network file (systemd 249 and later):

[DHCPServerStaticLease]
MACAddress=aa:bb:cc:dd:ee:01
Address=10.0.1.10

[DHCPServerStaticLease]
MACAddress=aa:bb:cc:dd:ee:02
Address=10.0.1.11

Static leases should sit outside the dynamic pool — below 10.0.1.100 with the pool above, so the two can never collide.

Serve NTP as well as advertise it[edit]

NTP=10.0.1.1 only tells clients where to look. The router has to actually answer, which it does not do by default.

With chrony, in /etc/chrony/chrony.conf (Debian) or /etc/chrony.conf (RHEL):

# Upstream sources
pool 2.pool.ntp.org iburst

# Answer the LAN
allow 10.0.1.0/24

# Serve local time even while upstream is unreachable, so the LAN does not
# lose its time source when the WAN goes down
local stratum 10
systemctl enable --now chronyd    # chrony on Debian/Ubuntu
chronyc sources -v
chronyc clients

The router's own firewall has to let NTP in. The rule is already in the baseline policy:

iptables -A INPUT -i eth1 -p udp --dport 123 -j ACCEPT
Never allow a network reachable from the WAN. An NTP server open to the internet is a reflection-amplification tool, and it will be found and used.

Verify[edit]

On the router:

# Leases the server has handed out
networkctl status eth1
resolvectl status eth1

# Clients currently talking NTP to us
chronyc clients

On a LAN client:

# Did the NTP option arrive?
resolvectl status            # systemd-resolved: DNS servers
timedatectl show-timesync    # ServerAddress should be 10.0.1.1
timedatectl timesync-status

# Raw lease, if the client runs networkd
networkctl status <iface>

A client that ignores the pushed NTP server almost always has one configured statically — check NTP= in /etc/systemd/timesyncd.conf, which takes precedence over anything DHCP supplies.

If you outgrow it[edit]

The networkd server is deliberately small: one pool per interface, no relay, no option-82, no failover, no dynamic DNS updates. Reach for Kea (the ISC successor to dhcpd) or dnsmasq when you need any of those. dnsmasq is the shorter jump — it does DHCP, DNS forwarding and TFTP in one process, which suits a small router.