Commit 7b040115963c
Changed files (8)
lib
scripts
systems
common
services
lib/dns-helpers.nix
@@ -0,0 +1,57 @@
+{ globals }:
+{
+ # Helper to get first IP from machine config
+ # Prefers regular IPs, fallback to VPN IPs
+ getMachineIP =
+ machine:
+ let
+ ips = machine.net.ips or [ ];
+ vpnIps = machine.net.vpn.ips or [ ];
+ # Prefer regular IPs, fallback to VPN IPs
+ allIps = if ips != [ ] then ips else vpnIps;
+ in
+ if builtins.isList allIps then builtins.head allIps else allIps;
+
+ # Generate machine subdomains with wildcard support
+ # Takes a list of machine names and returns an attribute set of DNS records
+ mkMachineRecords =
+ machineList:
+ builtins.listToAttrs (
+ map (machineName: {
+ name = machineName;
+ value = {
+ A = [ (globals.machines.${machineName}.net.ips or (globals.machines.${machineName}.net.vpn.ips)) ];
+ subdomains."*".A = [
+ (globals.machines.${machineName}.net.ips or (globals.machines.${machineName}.net.vpn.ips))
+ ];
+ };
+ }) machineList
+ );
+
+ # Helper to generate service DNS records from globals
+ # Takes a services attribute set and returns DNS records with alias support
+ mkServiceRecords =
+ services:
+ builtins.listToAttrs (
+ builtins.concatMap (
+ serviceName:
+ let
+ service = services.${serviceName};
+ hostName = if builtins.isAttrs service then service.host else service;
+ hostIP = globals.machines.${hostName}.net.ips;
+ ip = if builtins.isList hostIP then builtins.head hostIP else hostIP;
+ aliases = if builtins.isAttrs service then (service.aliases or [ ]) else [ ];
+ in
+ [
+ {
+ name = serviceName;
+ value.A = [ ip ];
+ }
+ ]
+ ++ (map (alias: {
+ name = alias;
+ value.A = [ ip ];
+ }) aliases)
+ ) (builtins.attrNames services)
+ );
+}
scripts/show-dns.sh
@@ -0,0 +1,50 @@
+#!/usr/bin/env bash
+# Display DNS zone entries generated from Nix configuration
+
+set -euo pipefail
+
+# Color codes for output
+BOLD='\033[1m'
+GREEN='\033[0;32m'
+BLUE='\033[0;34m'
+YELLOW='\033[0;33m'
+CYAN='\033[0;36m'
+RESET='\033[0m'
+
+# Default host to use for zone generation (must have bind configured)
+HOST="${1:-demeter}"
+
+echo -e "${BOLD}${BLUE}Generating DNS zones for ${CYAN}${HOST}${RESET}${BLUE}...${RESET}"
+echo
+
+# Define zones
+ZONES=(
+ "sbr.pm"
+ "home"
+ "vpn"
+ "192.168.1.in-addr.arpa"
+ "10.100.0.in-addr.arpa"
+)
+
+# Process each zone
+for zone_name in "${ZONES[@]}"; do
+ echo -e "${CYAN}Evaluating zone: ${zone_name}...${RESET}"
+
+ # Get the zone file content directly
+ zone_content=$(nix eval --raw ".#nixosConfigurations.${HOST}.config.services.bind.zones.\"${zone_name}\".file" 2>&1 | grep -v "^warning:" | grep -v "^Using saved setting")
+
+ if [[ -n "$zone_content" ]] && [[ "$zone_content" != *"error"* ]]; then
+ echo -e "${BOLD}${GREEN}=== Zone: ${zone_name} ===${RESET}"
+ echo
+ echo "$zone_content"
+ echo
+ else
+ echo -e "${YELLOW}Could not generate zone ${zone_name}${RESET}"
+ echo
+ fi
+done
+
+echo -e "${BOLD}${GREEN}Done!${RESET}"
+echo
+echo -e "${CYAN}Usage: $0 [hostname]${RESET}"
+echo -e "${CYAN}Example: make dns-show # uses demeter by default${RESET}"
scripts/update-gandi-dns.sh
@@ -0,0 +1,181 @@
+#!/usr/bin/env bash
+# Update Gandi DNS records from NixOS DNS configuration
+# Usage: ./scripts/update-gandi-dns.sh [--dry-run]
+
+set -euo pipefail
+
+# Colors
+BOLD='\033[1m'
+GREEN='\033[0;32m'
+BLUE='\033[0;34m'
+YELLOW='\033[0;33m'
+RED='\033[0;31m'
+CYAN='\033[0;36m'
+RESET='\033[0m'
+
+DOMAIN="sbr.pm"
+API_URL="https://api.gandi.net/v5/livedns/domains/$DOMAIN/records"
+DRY_RUN=false
+
+# Parse arguments
+for arg in "$@"; do
+ case $arg in
+ --dry-run)
+ DRY_RUN=true
+ shift
+ ;;
+ esac
+done
+
+# Check for API key (using lego's environment variable name)
+if [[ -z "${GANDIV5_PERSONAL_TOKEN:-}" ]]; then
+ echo -e "${RED}Error: GANDIV5_PERSONAL_TOKEN environment variable not set${RESET}"
+ echo -e "${YELLOW}Please set it with: export GANDIV5_PERSONAL_TOKEN=your-api-key${RESET}"
+ echo -e "${CYAN}Or source it from the agenix secret on rhea:${RESET}"
+ echo -e "${CYAN} source /run/agenix/gandi.env${RESET}"
+ exit 1
+fi
+
+echo -e "${BOLD}${BLUE}Updating Gandi DNS records for $DOMAIN...${RESET}"
+if [[ "$DRY_RUN" == "true" ]]; then
+ echo -e "${YELLOW}DRY RUN MODE - No changes will be made${RESET}"
+fi
+echo
+
+# Get the DNS zone file content from Nix
+echo -e "${CYAN}Extracting DNS records from Nix configuration...${RESET}"
+ZONE_FILE=$(nix eval --raw '.#nixosConfigurations.demeter.config.services.bind.zones."sbr.pm".file' 2>&1 | \
+ grep -v "^warning:" | grep -v "^Using saved setting")
+
+if [[ -z "$ZONE_FILE" ]]; then
+ echo -e "${RED}Error: Could not generate zone file${RESET}"
+ exit 1
+fi
+
+echo -e "${GREEN}DNS records extracted${RESET}"
+echo
+
+# Create a temporary file for the zone
+TEMP_ZONE=$(mktemp)
+echo "$ZONE_FILE" > "$TEMP_ZONE"
+
+# Extract A records (excluding SOA, NS, and comments)
+# Format examples:
+# name.domain. IN A ip
+# name.domain. TTL IN A ip
+# *.name.domain. IN A ip
+RECORDS=$(grep -E "^\S+\s+(([0-9]+\s+)?IN\s+)?A\s+" "$TEMP_ZONE" | \
+ grep -v "SOA" | \
+ grep -v "^;" || true)
+
+rm -f "$TEMP_ZONE"
+
+if [[ -z "$RECORDS" ]]; then
+ echo -e "${YELLOW}No A records found in zone file${RESET}"
+ exit 0
+fi
+
+echo -e "${GREEN}Found $(echo "$RECORDS" | wc -l) A records to process${RESET}"
+echo
+
+# Get current DNS records from Gandi
+if [[ "$DRY_RUN" == "false" ]]; then
+ echo -e "${CYAN}Fetching current DNS records from Gandi...${RESET}"
+ CURRENT_RECORDS=$(curl -s \
+ -H "Authorization: Apikey $GANDIV5_PERSONAL_TOKEN" \
+ "$API_URL" || echo "[]")
+
+ echo -e "${GREEN}Current records fetched${RESET}"
+ echo
+fi
+
+# Process each record
+UPDATED=0
+SKIPPED=0
+FAILED=0
+
+while IFS= read -r line; do
+ # Parse the line to extract: name, TTL, and value
+ # Handle various formats:
+ # name.sbr.pm. IN A value
+ # name.sbr.pm. TTL IN A value
+ # *.name.sbr.pm. TTL IN A value
+
+ # Extract components
+ FULL_NAME=$(echo "$line" | awk '{print $1}')
+
+ # Check if second field is a number (TTL) or "IN"
+ SECOND_FIELD=$(echo "$line" | awk '{print $2}')
+ if [[ "$SECOND_FIELD" =~ ^[0-9]+$ ]]; then
+ TTL="$SECOND_FIELD"
+ VALUE=$(echo "$line" | awk '{print $5}')
+ else
+ TTL=10800
+ VALUE=$(echo "$line" | awk '{print $4}')
+ fi
+
+ # Remove .sbr.pm. suffix and convert to Gandi format
+ NAME="${FULL_NAME%.sbr.pm.}"
+
+ # Convert wildcard format: *.name -> *.name (Gandi uses this format)
+ # Convert root wildcard: * -> @ (Gandi's root wildcard)
+ if [[ "$NAME" == "*" ]]; then
+ NAME="@"
+ fi
+
+ echo -e "${BLUE}Processing: ${RESET}$NAME.$DOMAIN A $VALUE (TTL: $TTL)"
+
+ if [[ "$DRY_RUN" == "true" ]]; then
+ echo -e " ${CYAN}[DRY RUN] Would update/create record${RESET}"
+ UPDATED=$((UPDATED + 1))
+ else
+ # Check if record exists and has same value
+ CURRENT_VALUE=$(echo "$CURRENT_RECORDS" | jq -r \
+ --arg name "$NAME" \
+ --arg type "A" \
+ '.[] | select(.rrset_name == $name and .rrset_type == $type) | .rrset_values[0]' \
+ 2>/dev/null || echo "")
+
+ if [[ "$CURRENT_VALUE" == "$VALUE" ]]; then
+ echo -e " ${GREEN}โ Record unchanged, skipping${RESET}"
+ SKIPPED=$((SKIPPED + 1))
+ else
+ if [[ -z "$CURRENT_VALUE" ]]; then
+ echo -e " ${YELLOW}Creating new record...${RESET}"
+ else
+ echo -e " ${YELLOW}Updating record (was: $CURRENT_VALUE)...${RESET}"
+ fi
+
+ # Update/create the record
+ RESPONSE=$(curl -s -w "\n%{http_code}" \
+ -X PUT \
+ -H "Authorization: Apikey $GANDIV5_PERSONAL_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "{\"rrset_values\": [\"$VALUE\"], \"rrset_ttl\": $TTL}" \
+ "$API_URL/$NAME/A")
+
+ HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
+ BODY=$(echo "$RESPONSE" | sed '$d')
+
+ if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then
+ echo -e " ${GREEN}โ Record updated successfully${RESET}"
+ UPDATED=$((UPDATED + 1))
+ else
+ echo -e " ${RED}โ Failed to update record (HTTP $HTTP_CODE)${RESET}"
+ echo -e " ${RED}Response: $BODY${RESET}"
+ FAILED=$((FAILED + 1))
+ fi
+ fi
+ fi
+
+ echo
+done <<< "$RECORDS"
+
+echo -e "${BOLD}${GREEN}DNS update complete!${RESET}"
+echo
+echo -e "${CYAN}Summary:${RESET}"
+echo -e " Updated: $UPDATED"
+echo -e " Skipped (unchanged): $SKIPPED"
+if [[ $FAILED -gt 0 ]]; then
+ echo -e " ${RED}Failed: $FAILED${RESET}"
+fi
systems/common/services/dns/home.nix
@@ -1,5 +1,34 @@
-{ dns, ... }:
+{ dns, globals, ... }:
with dns.lib.combinators;
+let
+ # Machines with home network IPs that should have wildcards
+ machinesWithWildcard = [
+ "okinawa"
+ "sakhalin"
+ "aomi"
+ "rhea"
+ "aion"
+ "shikoku"
+ "athena"
+ "demeter"
+ "nagoya"
+ ];
+
+ mkHomeMachineRecords = builtins.listToAttrs (
+ map (machineName: {
+ name = machineName;
+ value =
+ let
+ homeIP = globals.machines.${machineName}.net.ips;
+ ip = if builtins.isList homeIP then builtins.head homeIP else homeIP;
+ in
+ {
+ A = [ ip ];
+ subdomains."*".A = [ ip ];
+ };
+ }) machinesWithWildcard
+ );
+in
{
SOA = {
nameServer = "ns1.home.";
@@ -18,57 +47,23 @@ with dns.lib.combinators;
subdomains = {
# Name servers
- ns1.A = [ "192.168.1.182" ];
- ns2.A = [ "192.168.1.183" ];
+ ns1.A = [ (builtins.head globals.machines.demeter.net.ips) ];
+ ns2.A = [ (builtins.head globals.machines.athena.net.ips) ];
# Cache wildcard
- cache.subdomains."*".A = [ "192.168.1.70" ];
+ cache.subdomains."*".A = [ (builtins.head globals.machines.sakhalin.net.ips) ];
- # Machines with wildcards
- okinawa = {
- A = [ "192.168.1.19" ];
- subdomains."*".A = [ "192.168.1.19" ];
- };
- hokkaido.A = [ "192.168.1.11" ];
- honshu.A = [ "192.168.1.17" ];
- kobe.A = [ "192.168.1.18" ];
- sakhalin = {
- A = [ "192.168.1.70" ];
- subdomains."*".A = [ "192.168.1.70" ];
- };
- synodine.A = [ "192.168.1.20" ];
+ # Machines without wildcards
+ hokkaido.A = [ (builtins.head globals.machines.hokkaido.net.ips) ];
+ kobe.A = [ (builtins.head globals.machines.kobe.net.ips) ];
+ synodine.A = [ (builtins.head globals.machines.synodine.net.ips) ];
+
+ # Hardcoded entries not in globals or incomplete in globals
wakasu = {
A = [ "192.168.1.77" ];
subdomains."*".A = [ "192.168.1.77" ];
};
- aomi = {
- A = [ "192.168.1.23" ];
- subdomains."*".A = [ "192.168.1.23" ];
- };
- rhea = {
- A = [ "192.168.1.50" ];
- subdomains."*".A = [ "192.168.1.50" ];
- };
- aion = {
- A = [ "192.168.1.49" ];
- subdomains."*".A = [ "192.168.1.49" ];
- };
- shikoku = {
- A = [ "192.168.1.24" ];
- subdomains."*".A = [ "192.168.1.24" ];
- };
- athena = {
- A = [ "192.168.1.183" ];
- subdomains."*".A = [ "192.168.1.183" ];
- };
- demeter = {
- A = [ "192.168.1.182" ];
- subdomains."*".A = [ "192.168.1.182" ];
- };
- nagoya = {
- A = [ "192.168.1.80" ];
- subdomains."*".A = [ "192.168.1.80" ];
- };
+ honshu.A = [ "192.168.1.17" ];
remakrable.A = [ "192.168.1.57" ];
hass.A = [ "192.168.1.181" ];
@@ -136,5 +131,6 @@ with dns.lib.combinators;
k8sn1.A = [ "192.168.1.130" ];
k8sn2.A = [ "192.168.1.131" ];
k8sn3.A = [ "192.168.1.132" ];
- };
+ }
+ // mkHomeMachineRecords;
}
systems/common/services/dns/sbr.pm.nix
@@ -1,5 +1,32 @@
-{ dns, ... }:
+{ dns, globals, ... }:
with dns.lib.combinators;
+let
+ dnsHelpers = import ../../../../lib/dns-helpers.nix { inherit globals; };
+ inherit (dnsHelpers) getMachineIP mkServiceRecords;
+
+ # Only include machines that should be in sbr.pm zone
+ machineList = [
+ "shikoku"
+ "sakhalin"
+ "aix"
+ "rhea"
+ "aion"
+ "demeter"
+ "athena"
+ "nagoya"
+ "kerkouane"
+ ];
+
+ mkMachineRecords = builtins.listToAttrs (
+ map (machineName: {
+ name = machineName;
+ value = {
+ A = [ (getMachineIP globals.machines.${machineName}) ];
+ subdomains."*".A = [ (getMachineIP globals.machines.${machineName}) ];
+ };
+ }) machineList
+ );
+in
{
SOA = {
nameServer = "ns1.sbr.pm.";
@@ -17,9 +44,9 @@ with dns.lib.combinators;
];
subdomains = {
- # Name servers
- ns1.A = [ "192.168.1.182" ];
- ns2.A = [ "192.168.1.183" ];
+ # Name servers (demeter and athena)
+ ns1.A = [ (getMachineIP globals.machines.demeter) ];
+ ns2.A = [ (getMachineIP globals.machines.athena) ];
# Wildcard for public endpoint
"*".A = [
@@ -28,61 +55,7 @@ with dns.lib.combinators;
ttl = 10800;
}
];
-
- # Machines
- wakasu = {
- A = [ "192.168.1.77" ];
- subdomains."*".A = [ "192.168.1.77" ];
- };
- shikoku = {
- A = [ "192.168.1.24" ];
- subdomains."*".A = [ "192.168.1.24" ];
- };
- sakhalin = {
- A = [ "192.168.1.70" ];
- subdomains."*".A = [ "192.168.1.70" ];
- };
- aix = {
- A = [ "10.100.0.89" ];
- subdomains."*".A = [ "10.100.0.89" ];
- };
- rhea = {
- A = [ "192.168.1.50" ];
- subdomains."*".A = [ "192.168.1.50" ];
- };
- aion = {
- A = [ "192.168.1.49" ];
- subdomains."*".A = [ "192.168.1.49" ];
- };
- demeter = {
- A = [ "192.168.1.182" ];
- subdomains."*".A = [ "192.168.1.182" ];
- };
- athena = {
- A = [ "192.168.1.183" ];
- subdomains."*".A = [ "192.168.1.183" ];
- };
- honshu = {
- A = [ "192.168.1.15" ];
- subdomains."*".A = [ "192.168.1.15" ];
- };
- nagoya = {
- A = [ "192.168.1.80" ];
- subdomains."*".A = [ "192.168.1.80" ];
- };
- kerkouane = {
- A = [ "10.100.0.1" ];
- subdomains."*".A = [ "10.100.0.1" ];
- };
-
- # Rhea media services
- jellyfin.A = [ "192.168.1.50" ];
- jellyseerr.A = [ "192.168.1.50" ];
- sonarr.A = [ "192.168.1.50" ];
- radarr.A = [ "192.168.1.50" ];
- lidarr.A = [ "192.168.1.50" ];
- bazarr.A = [ "192.168.1.50" ];
- transmission.A = [ "192.168.1.50" ];
- t.A = [ "192.168.1.50" ];
- };
+ }
+ // mkMachineRecords
+ // mkServiceRecords globals.services;
}
systems/common/services/dns/vpn.nix
@@ -1,5 +1,35 @@
-{ dns, ... }:
+{ dns, globals, ... }:
with dns.lib.combinators;
+let
+ # Machines that have VPN entries
+ machineList = [
+ "okinawa"
+ "aomi"
+ "shikoku"
+ "sakhalin"
+ "rhea"
+ "aion"
+ "athena"
+ "demeter"
+ "nagoya"
+ "kyushu"
+ ];
+
+ mkVpnMachineRecords = builtins.listToAttrs (
+ map (machineName: {
+ name = machineName;
+ value =
+ let
+ vpnIP = globals.machines.${machineName}.net.vpn.ips;
+ ip = if builtins.isList vpnIP then builtins.head vpnIP else vpnIP;
+ in
+ {
+ A = [ ip ];
+ subdomains."*".A = [ ip ];
+ };
+ }) machineList
+ );
+in
{
SOA = {
nameServer = "ns1.vpn.";
@@ -18,54 +48,15 @@ with dns.lib.combinators;
subdomains = {
# Name servers
- ns1.A = [ "10.100.0.2" ];
- ns2.A = [ "10.100.0.16" ];
+ ns1.A = [ (builtins.head globals.machines.shikoku.net.vpn.ips) ];
+ ns2.A = [ (builtins.head globals.machines.sakhalin.net.vpn.ips) ];
- # Cache/Massimo wildcards
+ # Cache/Massimo wildcards - these don't exist in globals, keeping hardcoded
cache.subdomains."*".A = [ "10.100.0.6" ];
massimo.subdomains."*".A = [ "10.100.0.6" ];
- # Machines with wildcards
- okinawa = {
- A = [ "10.100.0.14" ];
- subdomains."*".A = [ "10.100.0.14" ];
- };
- aomi = {
- A = [ "10.100.0.17" ];
- subdomains."*".A = [ "10.100.0.17" ];
- };
- shikoku = {
- A = [ "10.100.0.2" ];
- subdomains."*".A = [ "10.100.0.2" ];
- };
- sakhalin = {
- A = [ "10.100.0.16" ];
- subdomains."*".A = [ "10.100.0.16" ];
- };
- rhea = {
- A = [ "10.100.0.50" ];
- subdomains."*".A = [ "10.100.0.50" ];
- };
- aion = {
- A = [ "10.100.0.49" ];
- subdomains."*".A = [ "10.100.0.49" ];
- };
- athena = {
- A = [ "10.100.0.83" ];
- subdomains."*".A = [ "10.100.0.83" ];
- };
- demeter = {
- A = [ "10.100.0.82" ];
- subdomains."*".A = [ "10.100.0.82" ];
- };
- nagoya = {
- A = [ "10.100.0.80" ];
- subdomains."*".A = [ "10.100.0.80" ];
- };
- kyushu = {
- A = [ "10.100.0.19" ];
- subdomains."*".A = [ "10.100.0.19" ];
- };
+ # hass - hardcoded as it's not in the machine list
hass.A = [ "10.100.0.81" ];
- };
+ }
+ // mkVpnMachineRecords;
}
globals.nix
@@ -534,4 +534,17 @@ _: {
};
};
};
+ services = {
+ # Media services on rhea
+ jellyfin.host = "rhea";
+ jellyseerr.host = "rhea";
+ sonarr.host = "rhea";
+ radarr.host = "rhea";
+ lidarr.host = "rhea";
+ bazarr.host = "rhea";
+ transmission = {
+ host = "rhea";
+ aliases = [ "t" ];
+ };
+ };
}
Makefile
@@ -105,6 +105,19 @@ keyboards/eyelash_corne/draw:
keyboards:
@$(MAKE) -C keyboards help
+# DNS
+.PHONY: dns-show
+dns-show:
+ @bash scripts/show-dns.sh
+
+.PHONY: dns-update-gandi
+dns-update-gandi:
+ @bash scripts/update-gandi-dns.sh
+
+.PHONY: dns-update-gandi-dry-run
+dns-update-gandi-dry-run:
+ @bash scripts/update-gandi-dns.sh --dry-run
+
# Maintenance
.PHONY: clean
clean: clean-system clean-results