{ config, lib, pkgs, ... }: let ddev-dns-update = pkgs.writeShellScriptBin "ddev-dns-update" '' set -e hosts_file="/var/lib/dnsmasq/ddev-hosts" new_hosts=$(${pkgs.coreutils}/bin/mktemp) trap '${pkgs.coreutils}/bin/rm -f "$new_hosts"' EXIT # Get running DDEV project names running=$(${pkgs.ddev}/bin/ddev list --json-output 2>/dev/null \ | ${pkgs.jq}/bin/jq -r '.raw[]? | select(.status == "running") | .name // empty' 2>/dev/null) || true # Build hosts entries for name in $running; do echo "127.0.0.1 $name.ddev.site" >> "$new_hosts" done # Only reload dnsmasq if content changed if ! ${pkgs.diffutils}/bin/cmp -s "$new_hosts" "$hosts_file"; then ${pkgs.coreutils}/bin/cp "$new_hosts" "$hosts_file" /run/wrappers/bin/sudo /run/current-system/systemd/bin/systemctl reload dnsmasq.service 2>/dev/null || true fi ''; in { # Enable systemd-resolved with split DNS for ddev.site services.resolved = { enable = true; dnssec = "false"; extraConfig = '' DNS=127.0.0.1:5353 Domains=~ddev.site ''; }; # Integrate NetworkManager with systemd-resolved networking.networkmanager.dns = "systemd-resolved"; # Local dnsmasq for .ddev.site resolution (port 5353) # Dynamic hosts file resolves running DDEV projects to 127.0.0.1; # unmatched .ddev.site queries forward to VPN DNS (returns dev server IP) services.dnsmasq = { enable = true; resolveLocalQueries = false; settings = { port = 5353; listen-address = "127.0.0.1"; bind-interfaces = true; server = [ "/ddev.site/10.42.97.1" ]; addn-hosts = "/var/lib/dnsmasq/ddev-hosts"; }; }; # Ensure hosts file exists before dnsmasq starts (owned by dominik so the # user-level service can write it) systemd.tmpfiles.rules = [ "d /var/lib/dnsmasq 0755 root root -" "f /var/lib/dnsmasq/ddev-hosts 0644 dominik root -" ]; # Poll running DDEV projects and update dnsmasq hosts # Runs as dominik because ddev needs user-level Docker access systemd.services.ddev-dns-update = { description = "Update dnsmasq hosts for running DDEV projects"; after = [ "dnsmasq.service" "docker.service" ]; serviceConfig = { Type = "oneshot"; User = "dominik"; ExecStart = "${ddev-dns-update}/bin/ddev-dns-update"; }; }; systemd.timers.ddev-dns-update = { wantedBy = [ "timers.target" ]; timerConfig = { OnBootSec = "30s"; OnUnitActiveSec = "10s"; }; }; environment.systemPackages = [ ddev-dns-update ]; security.sudo.extraRules = [ { users = [ "dominik" ]; commands = [ { command = "${ddev-dns-update}/bin/ddev-dns-update"; options = [ "NOPASSWD" ]; } { command = "/run/current-system/systemd/bin/systemctl reload dnsmasq.service"; options = [ "NOPASSWD" ]; } ]; } ]; # WireGuard VPN configuration networking.wireguard.interfaces = { wg0 = { ips = [ "10.42.98.201/32" ]; # publicKey: YdlRGsjh4hS3OMJI+t6SZ2eGXKbs0wZBXWudHW4NyS8= privateKeyFile = config.sops.secrets.wg-cloonar-key.path; peers = [ { publicKey = "TKQVDmBnf9av46kQxLQSBDhAeaK8r1zh8zpU64zuc1Q="; allowedIPs = [ "10.42.96.0/20" # wohnservice-wien "10.254.240.0/24" "10.254.235.0/24" # epicenter.works "10.14.0.0/16" "10.25.0.0/16" "188.34.191.144/32" # web-arm "91.107.201.241" # mail ]; endpoint = "vpn.cloonar.com:51820"; # ToDo: route to endpoint not automatically configured https://wiki.archlinux.org/index.php/WireGuard#Loop_routing https://discourse.nixos.org/t/solved-minimal-firewall-setup-for-wireguard-client/7577 persistentKeepalive = 25; } ]; # Use resolvectl for systemd-resolved integration # Note: No postDown needed - systemd-resolved automatically handles interface removal postSetup = '' ${pkgs.systemd}/bin/resolvectl dns wg0 10.42.97.1 ${pkgs.systemd}/bin/resolvectl domain wg0 cloonar.com ''; }; }; }