Skip to content

Network architecture

The DNF network operates at two scales: an isolated local zone, then multiple zones connected by a mesh VPN. The vocabulary is defined in Concepts.

A zone is a gateway and its machines.

Diagram
  • The gateway shares Internet (NAT) and protects the zone (firewall).
  • dnsmasq distributes addresses (DHCP) and resolves local names (<host>.<zone>).
  • On the Internet side (WAN), everything is closed. On the local side (LAN), only SSH, DNS, DHCP and the home page are open.

Optional service, enabled on the gateway. It sits in front of dnsmasq.

Diagram
  • AdGuardHome becomes the DNS resolver for workstations (port 53).
  • It blocks ads and trackers, and encrypts outgoing queries.
  • dnsmasq moves back to an internal port and keeps local names and DHCP.
  • No rate limiting: zone resolver, closed to the WAN. A burst of queries (fleet update) loses no response.

Several zones are linked by a mesh VPN, the tailnet, coordinated by the coordination server (HCS).

Diagram
  • The HCS (Headscale) is public and coordinates the meshing; traffic goes directly from one node to another.
  • Each gateway is a subnet router: it publishes the subnet of its zone into the VPN.
  • Zones can see each other; remote machines (smartphone, PC) join the network via Tailscale.
  • Nothing is allowed by default: an access policy, generated from etc/config.yaml, specifies who can reach what.
  • A pivot resolver on the HCS routes each zone’s names to its gateway.

Routing, DNS, MagicDNS, access policy, and activation keys are detailed in Tailnet operation.

A gateway routes, and a routed packet never goes through the port opening rules : those only concern what is addressed to it. The two filters are independent.

Only the following traffic goes through a gateway :

TrafficUsage
Zone → WANInternet sharing (NAT)
Zone → backup linkInternet sharing when the box goes down (backup links)
Zone ↔ VPNThe other zones, remote machines
Port replies and redirectionsReturn of outbound traffic, forwardPorts

Everything else is dropped. In particular, an Internet machine that set up a static route to a zone subnet reaches nothing : without this filtering, a gateway with a public address would let everything through.

Any extra routing requirement goes into the host file :

networking.firewall.extraForwardRules = ''
iifname "lan0" oifname "wg0" accept
'';

Port 22 is never open “everywhere”. A rule without an interface name applies to all interfaces, WAN included: on a gateway, this would expose SSH to the Internet.

HostSSH reachable from
Zone gatewayIts local network and the VPN
Global zone host (HCS)The Internet — an intentional decision, since it hosts the VPN
Zone machine (workstation, server)Its own interface only, i.e. the local network

Practical consequence: a gateway does not respond to SSH from the Internet. Remote access goes through the VPN:

Fenêtre de terminal
just enter gw

An internal service opens on the interface that matches its actual scope, never on all of them. Three scopes coexist:

ScopeOpeningExamples
Tailnettailscale0Loki (logs), relay for exposed services
Local networklan0 on a gateway, the single interface elsewhereDNS, DHCP, homepage, Nix cache
Address listip saddr rule, or client-side service listNFS shares, LAN games

The third case exists because a zone prefix is not an identity: any machine that obtains a local address is part of it, Wi-Fi guests included. Where the service allows it, the declared host list replaces the prefix.

Three families of names coexist on the network. All are generated from etc/config.yaml.

NameFormScopeExample
Simple name<host>Entire internal networkserver
Zone FQDN<name>.<zone>.<domain>Entire internal networkcloud.home.domain.tld
Global service<name>.<domain>Including the Internetgit.domain.tld
  • Simple names: these are the machines (declared hosts). Resolved everywhere on the network, to the machine’s real address, whatever its zone.
  • Zone FQDN: these are the services of a zone. They point to that zone’s gateway, which serves them through its reverse proxy.
  • Global services: services marked global in etc/config.yaml. Exposed on the HCS with public DNS: reachable from anywhere.

Who resolves what:

  • dnsmasq, on every gateway, knows all internal names (machines and services of all zones): it resolves simple names and FQDNs.
  • The current zone is resolved locally; the other zones and the global domain are routed to the right gateway, or to the HCS pivot resolver.
  • Public (Internet) names are sent upstream to encrypted DNS, filtered by AdGuardHome if it is enabled.
  • Only declared names exist : an unknown name under a zone answers NXDOMAIN, without redirecting to the gateway.
  • Tailnet names (<node>.tailnet.internal) are relayed by each gateway to MagicDNS : also resolved from the LANs.

On the coordination server (HCS), a pivot resolver (unbound) centralizes the internal VPN resolution. It knows the zone breakdown and simply routes :

  • each zone FQDN goes to its zone’s gateway, at its tailnet address ;
  • global services hosted on the HCS are resolved locally ;
  • everything else goes to an encrypted external DNS.

Remote devices query it via MagicDNS (split DNS), and gateways query it for names in the domain they don’t know. Between two zones, a gateway queries the other zone’s gateway directly. Details in DNS.

A machine outside any zone (laptop on the go, smartphone) joins the network via the VPN. Its DNS resolution is split (split-DNS) based on the nature of the requested name:

  • internal names: routed through the VPN to the HCS pivot resolver, which then directs them to the correct gateway;
  • tailnet node names (<node>.tailnet.internal, or the short name): resolved on the device itself by MagicDNS;
  • public names: resolved by the local resolver of the visited network (home router, public access point), without going through the VPN.
Diagram

A machine is declared in one zone, but it is not always there. A laptop goes on the road. A new gateway is installed from another zone, before being delivered on site.

Without special provision, this machine receives an arbitrary address from the DHCP range of the visited zone, and its name keeps pointing to its original address, unreachable from there. Nothing allows reaching it reliably.

Diagram

Each zone reserves the <prefix>.6.x block for machines from the other zones. The generator places a DHCP reservation there for every MAC address known on the network that does not belong to the zone.

The number is assigned at the scale of the whole network : a machine keeps the same last octet in every zone it visits.

MachineDeclared zoneAt homeVisiting atelier
portablemaison10.0.2.410.1.6.3
serveuratelier10.1.1.2(it is at home there)

The address is therefore known before the machine boots, without observing anything on the network.

The reservation comes with a DNS name in the visited zone. The simple name, for its part, keeps its meaning.

NameResolves to
portableits original address, everywhere on the network
portable.atelier.domain.tldits address in the atelier zone

dnsmasq indexes leases solely on the MAC address, not on the DHCP client identifier.

The reason : a given device does not advertise the same identifier depending on the system running on it. An installation image and the installed system look like two different machines, and receive two addresses. Relying on the MAC, the address no longer changes between installation and the first real boot.

The installation recipes compute the address all by themselves. They first identify the zone where the administration machine is located, based on its real default gateway and not on its declared zone (an administration laptop is precisely what moves around), then deduce the address of the target.

This address is then checked before being used : a reservation says where the machine would be if it were plugged in here, which is correct during the installation and wrong as soon as it is back home. If nothing responds at the reserved address, the tooling falls back to the host name when the latter responds — port 22, or 2222 for an encrypted machine waiting in its initrd. If nothing responds anywhere, the reserved address is kept : it is the one on which the installer will appear.

The calculation above gives one address to boot from. After the reboot that closes an installation, it is no longer sufficient : the machine may reappear elsewhere, or not really be up yet.

just full-install therefore probes all known addresses of the host, in this order, on ports 22 and 2222, and continues with the first one that answers :

  1. the address used for the installation;
  2. the fixed address of its origin zone, the one DNS publishes everywhere;
  3. the transit address reserved by the local zone;
  4. its name, resolved by the local gateway.

An answer on port 2222 means the host is waiting for its LUKS passphrase in its initrd : just unlock answers it without intervention. See Disk encryption.

Colmena 🡕 (just apply), fleet-update and just enter target a host by its name, which denotes its original address. On an administration machine, every SSH connection to a network host goes through dnf-locate, which tries :

  1. the name alone first : the common case costs nothing extra;
  2. otherwise, in parallel, its reservation in each zone (portable.workshop.domain.tld) and its tailnet name (portable.tailnet.internal).

The first path that answers wins. dnf-locate --print portable displays the one it finds.

The tailnet path only applies to an administration machine (tag:admin) : the access policy opens no other tailnet client to the zones.

To enforce an address, just configure sets a temporary SSH redirection for a roaming host, removed at the end of the installation or by hand :

Fenêtre de terminal
just roaming # show current redirections
just roaming-clear # remove all of them