Static IP with ifupdown#

A server other devices are supposed to reach needs an address that does not change. DHCP hands it whatever is free — possibly a different one after the router reboots, and every configuration pointing at the old one breaks. For a DNS server in particular that is not an option: its address is what every client has hardcoded.

A Debian install without a desktop manages networking through ifupdown and /etc/network/interfaces. NetworkManager and systemd-networkd solve the same problem but are not in play on a netinst system.

Find the interface name#

ip -br link
lo               UNKNOWN        00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
enp1s0           UP             a4:bb:6d:12:34:56 <BROADCAST,MULTICAST,UP,LOWER_UP>

enp1s0 is not a random name: en for Ethernet, p1 for PCI bus 1, s0 for slot 0. These predictable interface names stay stable across reboots and kernel updates — unlike the old eth0, whose numbering depended on the order devices happened to be detected in.

The configuration#

The block for a fixed address in /etc/network/interfaces:

auto enp1s0
iface enp1s0 inet static
    address 10.10.10.3/16
    gateway 10.10.1.1
    dns-nameservers 10.10.1.1 1.1.1.1
LineMeaning
auto enp1s0bring the interface up at boot. Without it the configuration exists but only applies on a manual ifup
iface enp1s0 inet staticIPv4 (inet), address assigned statically instead of via DHCP. For IPv6 it would be inet6
address 10.10.10.3/16address in CIDR notation. /16 means network 10.10.0.0 with hosts from 10.10.0.1 to 10.10.255.254. Older guides write address plus netmask 255.255.0.0 — equivalent, just more work
gateway 10.10.1.1default route for everything outside the local network. Must fall inside the network the mask spans
dns-nameservers 10.10.1.1 1.1.1.1nameservers for resolution, separated by spaces. The second one is the fallback if the first does not answer

The fixed address has to sit outside the router’s DHCP range. Otherwise the router will eventually hand the same address to another device and break both.

Two nameservers as a safety net#

A server without working name resolution cannot update packages, cannot sync time and cannot report anything outward. A second entry catches the failure of the first — here the router as the local resolver, a public one behind it.

How the list is worked through is decided by the C library’s resolver, not by the network:

  • Strictly in order. The first entry is always asked first. There is no load balancing and no preference for whichever server is faster.
  • Switching costs time. If the first does not answer, the timeout has to expire first (5 seconds by default) before the second is tried. While the first is down, that wait applies to every query.
  • Only outages are routed around. A timeout or a SERVFAIL moves on to the next server. An NXDOMAIN, on the other hand, is a valid answer and is passed through — a nameserver that answers wrongly does not get skipped.
  • Three entries maximum. glibc ignores anything beyond that.

The behaviour can be tuned if the wait is a problem:

options timeout:2 attempts:1

As soon as your own DNS server runs on the network — on this machine that is now Pi-hole — the public second entry becomes a hole in the filter: queries slip past it, unfiltered and unlogged. And if the entry points at a resolver that forwards back to your own DNS server, you get a loop. On the DNS server itself, 127.0.0.1 belongs in the list, and the public resolver is configured as an upstream inside it rather than alongside it. Both done since: The DNS Server Asks Itself and Choosing an Upstream DNS.

Apply#

systemctl reboot

A reboot is the honest test: it proves the configuration survives boot instead of only being set in the running system. It also works without one:

sudo ifdown enp1s0 && sudo ifup enp1s0

Either way an existing SSH session drops — the address is changing, after all. On a headless box, know the new address beforehand, or the only way back in is a monitor.

Verify#

ip -br a               # assigned address
ip route               # default via 10.10.1.1 dev enp1s0
ping -c1 10.10.1.1     # gateway reachable
cat /etc/resolv.conf   # which nameserver is actually in use

Pitfall: dns-nameservers without resolvconf#

ifupdown does not write the dns-nameservers line to /etc/resolv.conf itself. That is done by a hook script under /etc/network/if-up.d/ which only arrives with the resolvconf package (or openresolv). Without it the line is silently ignored — no error, no warning. What remains is whatever nameserver the installer wrote.

dpkg -l resolvconf openresolv 2>/dev/null | grep '^ii'

If that returns nothing, there are two ways forward:

sudo apt install resolvconf
sudo ifdown enp1s0 && sudo ifup enp1s0

Configuration stays in one place — /etc/network/interfaces is the source of truth.

printf 'nameserver 10.10.1.1\nnameserver 1.1.1.1\n' | sudo tee /etc/resolv.conf

One package less, but two files that have to agree. The dns-nameservers line then serves as documentation only — change it later and forget the resolv.conf, and you will be debugging in the wrong file.

The reverse also holds: with resolvconf installed, /etc/resolv.conf is generated and hand edits to it vanish on the next ifup. The head of the file tells you which case you are in — a generated one carries a DO NOT EDIT notice:

head -3 /etc/resolv.conf

On this machine the answer is confusing at first:

# Generated by dhcpcd

Yet there is no dhcpcd service here at all — only the dhcpcd-base package is installed, resolvconf and openresolv are absent as well, and the file still carries the date of the installation:

systemctl is-active dhcpcd     # inactive
systemctl is-enabled dhcpcd    # not-found
ls -la /etc/resolv.conf        # a real file, not a symlink

The header is therefore a leftover from the installer, not evidence of a running generator. In practice that means the second way above applies: the file is maintained by hand, and hand edits survive every reboot. Trusting the comment means looking for the configuration in a place where nobody writes.

The DNS Server Asks Itself#

As long as the gateway’s address sat in that file, the thin client was the only machine in the household whose own queries bypassed the filter — a library whose keeper looks things up somewhere else. Package sources, blocklist updates, time synchronisation: all unfiltered and in no log.

Now that Pi-hole runs on the same machine, its own address belongs there:

sudo cp /etc/resolv.conf /etc/resolv.conf.bak
printf '# Maintained by hand: no resolvconf, no dhcpcd service on this machine.\nnameserver 127.0.0.1\n' | sudo tee /etc/resolv.conf

127.0.0.1 rather than 10.10.10.3, even though both reach the same service: the loopback address works regardless of whether the network interface is up, and it stays correct when the machine later moves to a different segment. The query never enters the network stack at all.

The line in /etc/network/interfaces follows suit:

sudo sed -i 's/^\(\s*dns-nameservers\).*/\1 127.0.0.1/' /etc/network/interfaces

Without resolvconf it has no effect — but it is the place where somebody will look for the configuration. And installing that package one day would otherwise restore, on the next ifup, exactly the state that was just cleaned up.

The server now has no second nameserver. If Pi-hole fails, it can resolve nothing — no apt update, no blocklists. That is deliberate: a second entry would be precisely the hole described in the section above, on the very machine that runs the filter. When something breaks, add a fallback address by hand for the duration of the repair.

The counter-check, on the server itself:

dig +short example.com        # a normal answer
dig +short doubleclick.net    # 0.0.0.0 = the server now filters for itself too

From now on it shows up in the live log as a client of its own:

pihole -t