Posted on

Problem Context

I would like to be able to put something like https://dash into the browser on my local network or Tailscale and have it load my homelab dashboard.

There are a couple of caveats though:

  • It should work independently for my home network and Tailscale
  • It should also include DNS-based ad blocking

Aspiration

I would like to be able to run a Nixos container in the macvlan mode, so that on my home network level I get an IP address I can work with. This is necessary, because DHCP can not advertise DNS on a port different from 53.

I would like my DNS solution to be able to be DRY, I want to rely on my router to resolve hostnames and I would just like to specify CNAMEs for them in order to reach multiple services that are hosted on my home server.

Unexpected issues

Working with Nixos containers if very pleasant, its possible to configure the software that runs inside the container using Nix, so I don't need 2-phase builds, first the image then the container. Also, Nix is much nicer to work with than Dockerfiles.

However, after having configured macvlan for my container, I found out that the network is not coming up.

ifconfig -a

Turns out I'm not the only one. So it turned out that the best way to fix this is to switch to systemd.network. After having done that, I can run nixos-rebuild switch and the network inside of the container is connected correctly.

My NixOS configuration looks like this:

{ config, lib, ... }: {
  systemd.network = {
    enable = true;
    networks = {
      "40-enp2s0" = {
        matchConfig.Name = "enp2s0";
        networkConfig.DHCP = "yes";
      };
    };
  };
  networking = {
    networkmanager.enable = false;
    useNetworkd = true;
  };
}

there is no bridge to the macvlan, so the connections from the host to the container have to go around the main interface, I'd like to fix that in the future.

{ config, lib, ... }: {
  systemd.network = {
    enable = true;
    # silly fix for the service failing on nixos rebuild
    wait-online.enable = lib.mkForce false;
    networks = {
      "40-enp2s0" = {
        matchConfig.Name = "enp2s0";
        networkConfig.DHCP = "yes";
      };
    };
  };
  networking = {
    hostName = "butters"; # Define your hostname.
    networkmanager.enable = false;
    useNetworkd = true;
    nameservers = [
      "1.1.1.1"
      "1.0.0.1"
    ];
  };
}

and the container side looks like this:

{ config, lib, ... }: {
  containers.lan-dns = {
    autoStart = true;
    ephemeral = true;
    macvlans = [ "enp2s0" ];
    privateNetwork = false;
    #extraFlags = ["-U"]; # private user namespace
    config = { pkgs, ... }: {
      networking = {
        useDHCP = false;
        useNetworkd = true;
        useHostResolvConf = false;
        firewall = {
          enable = true;
          interfaces."mv-enp2s0".allowedUDPPorts = [ 53 ];
        };
      };
      systemd.network = {
        enable = true;
        networks = {
          "40-mv-enp2s0" = {
            matchConfig.Name = "mv-enp2s0";
            address = [
              "192.168.1.3/24"
            ];
            networkConfig.DHCP = "yes";
            dhcpV4Config.ClientIdentifier = "mac";
          };
        };
      };
    };
  };
}

This way the lan-dns container shows up on the outer network as a separate host and can serve DNS without problems.