Joseph's Blog

Getting systemd-resolved and dnsmasq to cooperate

Why?

Why run dnsmasq and systemd-resolved on the same server? Xe Iaso and David Anderson make a compelling argument for systemd-resolved on the Tailscale blog as it handles the widest variety of DNS configurations and seems to play well with other software. Importantly, Tailscale knows how to use systemd-resolved to add itself to the list of DNS resolvers for the system.

My goal with this setup is to have resolved manage the server’s DNS resolution—determining which queries are sent where. With Tailscale, I can publish some DNS records for various devices on my network that then resolve as 10.x.y.z addresses which are reachable via Tailscale. This works well for all devices running Tailscale, but there are some limitations here:

  1. If a device doesn’t have Tailscale installed/ set up properly, these 10.x.y.z addresses are unreachable
  2. Even if the device can be reached via Tailscale, while on the same LAN it’s usually quicker to use the standard LAN IP (192.168.x.y) and bypass Tailscale.

Thus, the goal of DNS resolution on my network is to resolve certain hostnames—nixos.josephstahl.com, for example—to local IPs for all devices on the LAN. Devices on WAN are given the usual Tailscale IPs (which are only reachable by devices on my tailnet).

Long story short

Queries for MagicDNS (ending in ts.net) should be send on to Tailscale, while queries for the rest of the internet (google.com, for example) should be sent to the usual resolvers (1.1.1.1 or similar), and queries for devices on the LAN (nixos.josephstahl.com, for example) should be answered with the device’s LAN address, rather than the WAN/Tailscale address.

systemd-resolved and dnsmasq

resolved is easily enabled on NixOS with services.resolved.enable = true. Similarly, dnsmasq may be enabled with services.dnsmasq.enable = true. However, the two will conflict as both try to bind to port 53 by default.

To address this, dnsmasq can be told to bind/listen to only a single IP on a single interface. dnsmasq is given the enp6s18 (ethernet) interface, while resolved listens to the loopback interface.

{
  pkgs,
  config,
  ...
}: let
  fqdn = config.networking.fqdn;
  address = [
    "/hostA.example.com/192.168.1.200"
    "/hostB.example.com/192.168.1.201"
  ]; # etc
in {
  services.dnsmasq = {
    enable = true;
    resolveLocalQueries = false;
    settings = {
      listen-address = [ "192.168.1.10" ];
      interface = "enp6s18";
      bind-interfaces = true;
      no-resolv = true; # use specific upstreams, as the /etc/resolv.conf file just points to systemd-resolved (which points back to dnsmasq...)
      no-hosts = true; # ignore hosts file, which keeps telling other devices that nixos.josephstahl.com is at 127.0.0.2
      cache-size = 500;
      server = [ "8.8.8.8" "1.1.1.1" "8.8.4.4" "1.0.0.1" "2606:4700:4700::1111" "2606:4700:4700::1001"];
      address = address;
      dnssec = true;
      conf-file="${pkgs.dnsmasq}/share/dnsmasq/trust-anchors.conf";
    };
  };
  networking.firewall.allowedTCPPorts = [53];
  networking.firewall.allowedUDPPorts = [53];
}

No additional configuration is necessary for resolved.

Conclusion

Probably best to reboot after all this. Now, all devices on the LAN which are using this server for DNS will be given the LAN IP addresses of various devices when they query dnsmasq. Meanwhile, the server itself will continue to use systemd-resolved, which sends all ts.net-related queries to Tailscale, and remaining queries are sent to dnsmasq (assuming either that upstream DNS has been manually set for resolved, or that the DHCP server on the network provides the server’s address as the DNS server).

#nix